ruact 0.0.8 → 0.0.10
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 +59 -1
- data/LICENSE.txt +21 -0
- data/README.md +124 -18
- data/SECURITY.md +1 -1
- data/lib/generators/ruact/install/install_generator.rb +264 -5
- data/lib/generators/ruact/install/templates/Procfile.dev.tt +3 -0
- data/lib/generators/ruact/install/templates/globals.css.tt +20 -0
- data/lib/generators/ruact/install/templates/initializer.rb.tt +9 -0
- data/lib/generators/ruact/install/templates/package.json.tt +7 -1
- data/lib/generators/ruact/install/templates/tsconfig.json.tt +18 -0
- data/lib/generators/ruact/scaffold/scaffold_shadcn_preflight.rb +10 -0
- data/lib/ruact/configuration.rb +73 -0
- data/lib/ruact/controller/document_rendering.rb +210 -0
- data/lib/ruact/controller.rb +5 -46
- data/lib/ruact/doctor.rb +37 -5
- data/lib/ruact/layout_source.rb +59 -0
- data/lib/ruact/version.rb +1 -1
- data/lib/ruact/view_helper.rb +10 -1
- data/lib/ruact.rb +1 -0
- data/spec/fixtures/readme/children-error.html.erb +3 -0
- data/spec/fixtures/readme/children-error.txt +1 -0
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +3 -0
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +3 -0
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +3 -0
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +3 -0
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +3 -0
- data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +16 -0
- data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +24 -0
- data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +15 -0
- data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +17 -0
- data/spec/readme_demo_message_spec.rb +67 -0
- data/spec/readme_spec.rb +282 -0
- data/spec/ruact/controller_request_spec.rb +203 -0
- data/spec/ruact/doctor_spec.rb +81 -6
- data/spec/ruact/install_generator_spec.rb +442 -70
- data/spec/ruact/layout_source_spec.rb +108 -0
- data/spec/ruact/scaffold_generator_spec.rb +14 -0
- metadata +30 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb5572218264cb747d7e6d89f990946c3eb05b4614469f4d96fd69158aece888
|
|
4
|
+
data.tar.gz: efe3a28deef2028b9470d0d1edbe5770d7289f668b72d842c0a4f4cab8aef47c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6fd494f658f9e5121f0f2beed2965c6d58926f8c4ec1f4d6b0b3db714d903c9a3a4ecc1492c32e5e3f61dc6bad10ea716e2a9795884cd5319bcc039af4c5dc3b
|
|
7
|
+
data.tar.gz: bd6ac4fcf1042364845221187cb85d64dd3f299bc6873d1a9d7cd796cbf1ec13e95b07692ef55c5dd4c1e861ad9180639d97c5df19e7d2cc2eb4a0d777972fb1
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.0.10] - 2026-08-25
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **`README.md` — the gem's public face is no longer `bundle gem` boilerplate.** The file GitHub renders on `github.com/luizcg/ruact`, the file `spec.files` packages *inside* the built `.gem`, and the destination of `source_code_uri` still said *"TODO: Delete this and the text below"*, told the reader to `bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG`, and pointed contributors at `https://github.com/[USERNAME]/ruact`. It is now a first-contact README for v0.0.9: the four-command greenfield quick start (the same sequence the site's landing puts on the clipboard), the ERB-tag/`"use client"` pair, `include Ruact::Server` with its generated typed accessor, a list where every bullet names a shipped artifact, a compatibility table sourced from the CI matrix rather than from optimism, and links out to `ruact.dev` instead of second copies of the guides.
|
|
15
|
+
|
|
16
|
+
It also carries the **AI-tools section deferred here from the docs work** — the one-requirement rule (`"use client"` at the top; a PascalCase *named* export is the thing to check rather than add), the four-step walkthrough, and a pointer to the canonical page — plus one line naming the shipped agent context: the `AGENTS.md` `rails generate ruact:install` writes, `ruact.dev/llms.txt`, and the experimental `-- --json` output of `ruact:doctor` / `ruact:routes`.
|
|
17
|
+
|
|
18
|
+
Two gates now watch it, because nothing did before: `spec/readme_spec.rb` runs inside this repo's own `rspec` job (no boilerplate literals, every relative link resolves on disk, no `path:` gem source a reader could copy, and the quick-start block pinned literally), and the monorepo's command-spine check treats `README.md` as a declared artifact of the one canonical greenfield sequence.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **A demo at the top of `README.md` — the write→verify loop, recorded rather than described.** The README could argue the mechanism in prose and code but could not show what happens when the boundary is used wrong, which is the part a reader of *"React rendered from Rails templates"* actually doubts. The recording is one loop: an ERB template with `<LikeButton likes={@likes} />` beside the `"use client"` component it resolves to; the component rendering in a browser, its count changing on click; children put inside the tag — the JSX habit — and the next request stopping **server-side, while the template is being processed, before anything reaches the browser**, with `Ruact::ChildrenNotSupportedError` naming the component, the template file and line, and the fix; then the children removed and the page rendering again. Every frame is a capture of the gem running; the loud error is the one shipped in 0.0.9.
|
|
23
|
+
|
|
24
|
+
It is referenced by absolute URL and **nothing binary is committed here** — so neither `gem install ruact` nor a clone of this repository carries a multi-megabyte file, permanently, for a picture. Two things keep it from aging into a lie: the URL belongs to the site's URL contract, and `spec/readme_demo_message_spec.rb` pins the message the recording shows against a fixture the gem itself produced. If the wording changes, that spec goes red and names the recording as the thing to redo.
|
|
25
|
+
|
|
26
|
+
- **`LICENSE.txt`** — the gemspec has declared `spec.license = "MIT"` since the first commit while the repository contained no licence file, so both GitHub and the packaged gem shipped a promise with nothing behind it.
|
|
27
|
+
- **`spec.description` and `metadata["documentation_uri"]` in the gemspec** — RubyGems rendered the one-line `summary` as the whole description and fell through to rubydoc.info for documentation.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **`SECURITY.md` named a version that has never existed.** Its supported-versions table listed `0.1.x`; the released gem is `0.0.9`. Corrected to `0.0.x`.
|
|
32
|
+
|
|
33
|
+
## [0.0.9] - 2026-08-12
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **`rails generate ruact:install --shadcn` — the `--shadcn` scaffold path now has a working setup.** `ruact:scaffold --shadcn` emits components dressed in Tailwind classes, but nothing in ruact ever wired Tailwind, so the generated CRUD rendered **unstyled**: the classes had nothing to resolve against. Worse, the documented next step did not work either — shadcn's own CLI **refuses to initialize** in a ruact app, aborting with *"No Tailwind CSS configuration found"* and *"Could not find valid path aliases"*, because a fresh ruact app ships neither Tailwind nor a `tsconfig.json`.
|
|
38
|
+
|
|
39
|
+
The new flag emits exactly those prerequisites, each verified against the real shadcn CLI: `app/javascript/styles/globals.css` (the Tailwind entry shadcn appends its design tokens to, and points `components.json` at), a `tsconfig.json` carrying the `@/*` → `app/javascript/*` alias (the bundled Vite plugin already registers the same alias for the *bundler*; this is what makes it resolve for *TypeScript*, and therefore for shadcn's probe and your editor), `app/assets/builds/` with the compiled stylesheet gitignored, the `build:css` script, and a `css` process in `Procfile.dev` so `bin/dev` rebuilds the stylesheet alongside Rails and Vite.
|
|
40
|
+
|
|
41
|
+
It then **prints** the two `npx shadcn` commands instead of running them — they hit the network, and `shadcn init` is interactive, so automating them is neither safe (the scaffold generator's "never auto-run npx/npm" rule) nor possible. The printed `init` line pins **`--base radix`**, which is the part nobody would guess: current shadcn defaults to **Base UI**, while the components `ruact:scaffold --shadcn` generates import **Radix** primitives — accepting the default gets you a component library the scaffold cannot use. The printed `add` list is the complete primitive superset, pinned by a spec as a superset of whatever a given resource narrows to, so the two generators cannot drift.
|
|
42
|
+
|
|
43
|
+
**The default (agnostic) path is untouched** — no Tailwind, no `tsconfig.json`, and a byte-identical `package.json` and `Procfile.dev`, all pinned by specs.
|
|
44
|
+
|
|
45
|
+
Verified end to end twice from `rails new`: the generated CRUD renders as a styled shadcn table with the `useQuery` search filtering rows live. Note that shadcn's CLI is currently **4.x** while `shadcn_compatible_versions` defaults to `[1, 2]`, so the scaffold's version pre-flight emits its (non-blocking) warning; the generated components do work against 4.x.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- **A ruact page can now carry your app's CSS — the Rails layout owns the document.** `ruact_render` rendered the view with `layout: false` and then wrapped the Flight payload in a hardcoded HTML shell whose `<head>` holds only `charset`, `viewport`, the CSRF meta tag and the title `Rails RSC`. That shell has **no stylesheet slot and never uses your layout**, so `stylesheet_link_tag` — and with it favicons, fonts, analytics and every `<head>`-writing gem — could not reach a ruact-rendered page **at all**. The visible consequence: `rails generate ruact:scaffold --shadcn` emitted components whose Tailwind classes had nothing to resolve against, so the generated CRUD was **unstyled by construction**; the docs' instruction to put shadcn theme variables in `app/assets/stylesheets/application.css` pointed at a file that provably never reached the browser; and Epic 12 (`ruact_meta` → tags in `<head>`) had no surface to write into. The default agnostic scaffold was affected too — it renders plain semantic HTML and only *looked* acceptable because browsers style a bare `<table>`.
|
|
50
|
+
|
|
51
|
+
A non-Flight HTML response is now rendered **through the host app's own layout**, with the React root's bootstrap tags supplied by the layout's `<%= ruact_js_assets %>` call (which, called with no argument, picks up the render's Flight payload). The Flight wire shape (`text/x-component`) is untouched — this only changes the full document a browser gets on a normal navigation.
|
|
52
|
+
|
|
53
|
+
Controlled by the new **`Ruact.config.layout`** — `false` by default, `true` (or a layout name) to opt in. `rails generate ruact:install` now writes **both halves of that opt-in in the same run**: `config.layout = true` in the generated initializer, and `<%= ruact_js_assets %>` next to the React root in your layout.
|
|
54
|
+
|
|
55
|
+
**The default is `false`, so an existing app is untouched until it opts in** — not because ruact detects anything, but because it does not look. An earlier cut of this change tried to infer whether your layout was ready by inspecting it; three review rounds each found another template shape that fooled the inference (a mention in a comment, a commented-out call, a trim-mode comment), and each wrong answer decided how every page in the app rendered. "Does this template call this method?" is not a question pattern-matching can answer reliably, so it is no longer asked. One explicit line beats a clever guess for a setting this load-bearing. (`:auto` is rejected with a message naming its replacement, so an initializer carrying it forward cannot be silently reinterpreted.)
|
|
56
|
+
|
|
57
|
+
Once opted in, a layout that cannot mount the app is a configuration error rather than a silent blank page: ruact checks the rendered document for both the assets block and a `<div id="root">`, raising in development/test and logging-and-degrading to the shell in production. A controller with no resolvable layout (API-shaped, or `layout false`) degrades quietly instead — that is a normal Rails choice, not a mistake.
|
|
58
|
+
|
|
59
|
+
**To migrate an existing app:** add `<%= ruact_js_assets %>` next to the `<div id="root"></div>` in `app/views/layouts/application.html.erb`, or re-run `rails generate ruact:install` (which now injects both, and adds only the missing call to a layout that already has the root — tolerating single quotes, extra attributes and a same-line marker, and telling you loudly if it cannot find the root div rather than reporting a success it did not perform). `rails ruact:doctor` reports the half-migrated state as a **warning** naming the one-line fix — where it previously reported a root-only layout as a pass.
|
|
60
|
+
|
|
61
|
+
**Known limitation:** a ruact view is rendered in its own pass (it produces the component tree), so `content_for` declared *inside the view* does not reach the layout. Set document metadata from the controller.
|
|
62
|
+
|
|
63
|
+
`rails ruact:doctor` reports the two halves separately, because they are different fixes: a layout missing the root or the helper **fails**, and a ready layout with `config.layout = false` **warns** and names the setting.
|
|
64
|
+
|
|
10
65
|
## [0.0.8] - 2026-07-11
|
|
11
66
|
|
|
12
67
|
### Changed
|
|
@@ -192,6 +247,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
192
247
|
- **CI matrix** — GitHub Actions: RSpec across Ruby 3.2 × 3.3 × Rails 7.0 × 7.1 × 7.2 × 8.0; RuboCop; YARD docs; memory benchmark; E2E system tests against React 19.0.0 and 19.x (Capybara + Cuprite); non-blocking React@next job with auto-issue on failure.
|
|
193
248
|
- **E2E test app** — `e2e/` Rails app (no DB, in-memory Post model) with full CRUD system tests validating the complete request cycle.
|
|
194
249
|
|
|
195
|
-
[Unreleased]: https://github.com/luizcg/ruact/compare/v0.0.
|
|
250
|
+
[Unreleased]: https://github.com/luizcg/ruact/compare/v0.0.10...HEAD
|
|
251
|
+
[0.0.10]: https://github.com/luizcg/ruact/releases/tag/v0.0.10
|
|
252
|
+
[0.0.9]: https://github.com/luizcg/ruact/releases/tag/v0.0.9
|
|
253
|
+
[0.0.8]: https://github.com/luizcg/ruact/releases/tag/v0.0.8
|
|
196
254
|
[0.0.7]: https://github.com/luizcg/ruact/releases/tag/v0.0.7
|
|
197
255
|
[0.0.6]: https://github.com/luizcg/ruact/releases/tag/v0.0.6
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luiz Garcia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
CHANGED
|
@@ -1,37 +1,143 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ruact
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Real React, right in your Rails views.** Write a PascalCase tag in ERB, pass `@post` as a prop, and a React component renders — no hand-written JSON layer, no Node process in production.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://github.com/luizcg/ruact/actions/workflows/ci.yml) [](https://badge.fury.io/rb/ruact) [](https://codecov.io/gh/luizcg/ruact)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<!-- Packaging (Story 5.2): the demo is a documentation asset, published with the site
|
|
8
|
+
rather than committed here — so neither `gem install ruact` nor a clone of this
|
|
9
|
+
repository carries a multi-megabyte file, permanently, for a picture. The URL belongs
|
|
10
|
+
to the site's URL contract and cannot quietly start 404ing, the capture sources are
|
|
11
|
+
scripted and live with the project's other capture harnesses, and the message the demo
|
|
12
|
+
shows is pinned here by `spec/readme_demo_message_spec.rb`. -->
|
|
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. Children are then put inside the tag — the JSX habit — and the next request stops server-side with Ruact::ChildrenNotSupportedError, which names the component, the template file and line, and the fix. The children come out again and the page renders." />
|
|
8
15
|
|
|
9
|
-
##
|
|
16
|
+
## Quick start
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
```bash
|
|
19
|
+
# 1. A throwaway app to try it in
|
|
20
|
+
rails new myapp --skip-javascript && cd myapp
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
# 2. Add the gem
|
|
23
|
+
bundle add ruact
|
|
14
24
|
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
# 3. Write the config, the layout wiring and an AGENTS.md — then run npm install
|
|
26
|
+
rails generate ruact:install
|
|
27
|
+
|
|
28
|
+
# 4. Rails + Vite, one command
|
|
29
|
+
bin/dev
|
|
17
30
|
```
|
|
18
31
|
|
|
19
|
-
|
|
32
|
+
That is the whole install. The [Getting Started guide](https://ruact.dev/docs/getting-started) picks it up from here — first component, first scaffold, `ruact:doctor`. Already have an app? Start at step 2, then read [Progressive migration](https://ruact.dev/docs/guides/progressive-migration) — ruact renders one action at a time and leaves the rest of your views alone.
|
|
20
33
|
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
## How it works
|
|
35
|
+
|
|
36
|
+
```erb
|
|
37
|
+
<%# app/views/posts/show.html.erb %>
|
|
38
|
+
<PostCard post={@post} author={@author} />
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// app/javascript/components/PostCard.tsx
|
|
43
|
+
"use client"
|
|
44
|
+
|
|
45
|
+
import { useState } from "react"
|
|
46
|
+
|
|
47
|
+
export function PostCard({ post, author }) {
|
|
48
|
+
const [liked, setLiked] = useState(false)
|
|
49
|
+
return (
|
|
50
|
+
<article>
|
|
51
|
+
<h1>{post.title}</h1>
|
|
52
|
+
<p>by {author.name}</p>
|
|
53
|
+
<button onClick={() => setLiked(!liked)}>
|
|
54
|
+
{liked ? "Liked" : "Like"}
|
|
55
|
+
</button>
|
|
56
|
+
</article>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Rails serializes `@post` and `@author` as Ruby values, sends the component tree as a [Flight](https://ruact.dev/docs/concepts/flight-wire-format) payload, and React hydrates it in the browser. No JSON ceremony, no duplicate routes, no Node.js in production.
|
|
62
|
+
|
|
63
|
+
## Call Rails from React
|
|
64
|
+
|
|
65
|
+
Add one line to a controller and its routed non-GET actions become callable from React at their real routes:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
class PostsController < ApplicationController
|
|
69
|
+
include Ruact::Server # ← the only new line
|
|
70
|
+
|
|
71
|
+
def create
|
|
72
|
+
@post = Post.create!(post_params)
|
|
73
|
+
redirect_to @post
|
|
74
|
+
end
|
|
75
|
+
# ...
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { createPost } from "@/.ruact/server-functions";
|
|
81
|
+
|
|
82
|
+
await createPost({ post: { title: "Hi", body: "…" } });
|
|
23
83
|
```
|
|
24
84
|
|
|
25
|
-
|
|
85
|
+
The verb decides — there is no per-action DSL and no second endpoint. The export name is derived from the route (`posts#create` → `createPost`), and ruact's Vite plugin regenerates the module whenever your routes change. What the call *resolves* is decided by the action you already wrote: this one redirects, so ruact follows the redirect and the call resolves `null`; an action that assigns `@post` instead resolves `{ post: … }`, through the same `ruact_props` allowlist as everything else. Reading works the same way: a `Ruact::Query` class mounted with `ruact_queries` draws one named `GET` route per public method, and React reads it with `useQuery`. Both are documented in [Server functions & queries](https://ruact.dev/docs/api/server-actions).
|
|
86
|
+
|
|
87
|
+
## What you get
|
|
88
|
+
|
|
89
|
+
Every item below is shipped in this gem at v0.0.9:
|
|
90
|
+
|
|
91
|
+
- **ERB as server components** — `include Ruact::Controller`, then write PascalCase tags in your existing views. [Docs](https://ruact.dev/docs/concepts/erb-as-server-components)
|
|
92
|
+
- **`"use client"`** — the one directive that marks a file as client-side. The bundled Vite plugin scans for it and writes the manifest. [Docs](https://ruact.dev/docs/concepts/use-client)
|
|
93
|
+
- **Server functions and queries** — `include Ruact::Server` and `Ruact::Query` + `useQuery`, both reachable through a typed module generated from your route table. [Docs](https://ruact.dev/docs/api/server-actions)
|
|
94
|
+
- **Props are an allowlist** — `include Ruact::Serializable` + `ruact_props :id, :title`; other columns never cross. [Docs](https://ruact.dev/docs/api/serializable)
|
|
95
|
+
- **Validation errors round-trip** — `ruact_errors(record)` hands React `{ title: ["can't be blank"] }` without a serializer. [Docs](https://ruact.dev/docs/api/server-actions)
|
|
96
|
+
- **Signed record references** — `Ruact.signed_global_id(record, for:, expires_in:)` out, `Ruact.locate_signed(token, for:)` back in; a tampered token is a `400`, not a lookup. [Docs](https://ruact.dev/docs/api/server-actions)
|
|
97
|
+
- **Client-side navigation** — link interception, scroll restoration and redirect-after-POST, derived from your Rails routes. [Docs](https://ruact.dev/docs/concepts/navigation)
|
|
98
|
+
- **A CRUD generator** — `rails generate ruact:scaffold Post title:string body:text` delegates the model, migration and route to Rails' own `resource` generator, then adds the ruact layer. Plain semantic HTML by default; `--shadcn` opts into the Tailwind/shadcn path. It does not run migrations — `rails db:migrate` is still yours. [Docs](https://ruact.dev/docs/api/scaffold)
|
|
99
|
+
- **`bin/rails ruact:doctor`** — eight checks over the manifest, Vite, the layout and streaming; exits `1` when one fails. [Docs](https://ruact.dev/docs/api/ruact-doctor)
|
|
100
|
+
- **One runtime dependency** — `nokogiri`. Rails itself is not a declared dependency of this gem.
|
|
101
|
+
|
|
102
|
+
## AI tools and coding agents
|
|
103
|
+
|
|
104
|
+
**The only line you have to add to an AI-generated React component is `"use client"` at the top of the file.** One thing to check rather than add: the component needs a PascalCase *named* export, because that name is the tag you write in ERB.
|
|
26
105
|
|
|
27
|
-
|
|
106
|
+
1. Generate the component with whatever AI tool you already use.
|
|
107
|
+
2. Save it as `app/javascript/components/MyComponent.tsx`.
|
|
108
|
+
3. Add `"use client"` at the top if it is not already there.
|
|
109
|
+
4. Call `<MyComponent />` from ERB.
|
|
28
110
|
|
|
29
|
-
|
|
111
|
+
That is the whole adaptation, and it holds for any tool that outputs standard React components — nothing here is pinned to one vendor. The worked example, the traps and the real error messages are on [AI Tools & Agents](https://ruact.dev/docs/guides/ai-tools).
|
|
30
112
|
|
|
31
|
-
|
|
113
|
+
For agents driving the app rather than writing one component: `rails generate ruact:install` writes an `AGENTS.md` into your app, [ruact.dev/llms.txt](https://ruact.dev/llms.txt) serves the same context to tools that fetch from the web, and `bin/rails ruact:doctor -- --json` / `bin/rails ruact:routes -- --json` emit machine-readable output (experimental — `schema_version: 0`, and the `--` separator is required).
|
|
32
114
|
|
|
33
|
-
|
|
115
|
+
## Compatibility
|
|
116
|
+
|
|
117
|
+
| | Version | Where that comes from |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| Ruby | >= 3.2 | the gemspec's `required_ruby_version` |
|
|
120
|
+
| Rails | tested against 7.0, 7.1, 7.2 and 8.0 | every commit runs the full CI matrix; the gemspec sets no Rails bound |
|
|
121
|
+
| React | 19.x | the `package.json` the install generator writes |
|
|
122
|
+
| Node.js | >= 20 | the build only — ruact runs no Node process in production |
|
|
123
|
+
|
|
124
|
+
## Documentation
|
|
125
|
+
|
|
126
|
+
Everything lives at [ruact.dev](https://ruact.dev):
|
|
127
|
+
|
|
128
|
+
- [Getting Started](https://ruact.dev/docs/getting-started) — from `rails new` to a rendered component
|
|
129
|
+
- [Why ruact?](https://ruact.dev/docs/why-ruact) — where it sits next to Hotwire and Inertia
|
|
130
|
+
- [Server functions & queries](https://ruact.dev/docs/api/server-actions) — the full request/response contract
|
|
131
|
+
- [Progressive migration](https://ruact.dev/docs/guides/progressive-migration) — adopting it one action at a time
|
|
132
|
+
- [Testing](https://ruact.dev/docs/guides/testing) — render assertions on the server side
|
|
133
|
+
- [Changelog](CHANGELOG.md) — also published at [ruact.dev/docs/changelog](https://ruact.dev/docs/changelog)
|
|
34
134
|
|
|
35
135
|
## Contributing
|
|
36
136
|
|
|
37
|
-
Bug reports and pull requests are welcome
|
|
137
|
+
Bug reports and pull requests are welcome at [github.com/luizcg/ruact/issues](https://github.com/luizcg/ruact/issues).
|
|
138
|
+
|
|
139
|
+
Release process: [RELEASING.md](RELEASING.md). Security policy and private reporting: [SECURITY.md](SECURITY.md).
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT — see [LICENSE.txt](LICENSE.txt).
|
data/SECURITY.md
CHANGED
|
@@ -6,7 +6,7 @@ Only the latest patch release of the current minor version is actively maintaine
|
|
|
6
6
|
|
|
7
7
|
| Version | Supported |
|
|
8
8
|
|---------|-----------|
|
|
9
|
-
| 0.
|
|
9
|
+
| 0.0.x | ✅ Yes |
|
|
10
10
|
|
|
11
11
|
Once a new minor version is released, the previous minor version receives security fixes for **90 days** after the new release, then it is no longer supported.
|
|
12
12
|
|
|
@@ -12,7 +12,10 @@ module Ruact
|
|
|
12
12
|
# Performs the following actions:
|
|
13
13
|
# 1. Creates config/initializers/ruact.rb
|
|
14
14
|
# 2. Injects `include Ruact::Controller` into ApplicationController
|
|
15
|
-
# 3. Injects the React root div into
|
|
15
|
+
# 3. Injects the React root div AND `ruact_js_assets` into
|
|
16
|
+
# app/views/layouts/application.html.erb, so the app's own layout owns
|
|
17
|
+
# the document (and its `<head>` — stylesheets, fonts, meta — reaches a
|
|
18
|
+
# ruact page). See Ruact::Configuration#layout.
|
|
16
19
|
# 4. Creates app/javascript/components/.keep
|
|
17
20
|
# 5. Creates vite.config.js (or shows manual instructions if one exists)
|
|
18
21
|
# 6. Creates package.json (react/react-dom/vite/@vitejs/plugin-react) so a
|
|
@@ -25,6 +28,10 @@ module Ruact
|
|
|
25
28
|
# verification commands in context by default (Story 15.1, FR105).
|
|
26
29
|
# 9. Runs `npm install` so JavaScript dependencies are ready (FR101);
|
|
27
30
|
# skippable via --skip-npm.
|
|
31
|
+
# 10. With `--shadcn`: also emits the prerequisites shadcn's own CLI refuses
|
|
32
|
+
# to initialize without (a Tailwind entry + a `tsconfig.json` import
|
|
33
|
+
# alias), wires the `css` build process, and prints the two `npx shadcn`
|
|
34
|
+
# commands it deliberately does not run.
|
|
28
35
|
#
|
|
29
36
|
# Story 14.2 (FR104) — the generator no longer writes a bootstrap entry into
|
|
30
37
|
# the user's tree. ruact's React entry is served as the virtual module
|
|
@@ -48,8 +55,32 @@ module Ruact
|
|
|
48
55
|
default: false,
|
|
49
56
|
desc: "Skip running npm install (for CI or non-npm package managers)"
|
|
50
57
|
|
|
58
|
+
# Prepares the app for the `ruact:scaffold --shadcn` path. shadcn's own
|
|
59
|
+
# CLI refuses to initialize without BOTH Tailwind and a TypeScript import
|
|
60
|
+
# alias, and a ruact app has neither — it ships no Tailwind and no
|
|
61
|
+
# `tsconfig.json` at all. That left `--shadcn` scaffolding into an app
|
|
62
|
+
# where the generated components' classes resolved to nothing: markup with
|
|
63
|
+
# no styling. This flag emits exactly the prerequisites (verified against
|
|
64
|
+
# the real shadcn CLI) and then PRINTS the two `npx shadcn` commands
|
|
65
|
+
# rather than running them — they hit the network and `shadcn init` is
|
|
66
|
+
# interactive, so automating them is neither safe nor possible.
|
|
67
|
+
class_option :shadcn,
|
|
68
|
+
type: :boolean,
|
|
69
|
+
default: false,
|
|
70
|
+
desc: "Also wire Tailwind + a TS import alias, the prerequisites for `ruact:scaffold --shadcn`"
|
|
71
|
+
|
|
72
|
+
# A fresh app gets the whole initializer. An app that ALREADY has one gets
|
|
73
|
+
# a surgical injection instead of `template`'s overwrite prompt, which
|
|
74
|
+
# offered a bad choice on the one path that matters most — migrating an
|
|
75
|
+
# existing app: overwrite and lose every setting the app had
|
|
76
|
+
# (`strict_serialization`, `manifest_path`, the SGID defaults…), or skip
|
|
77
|
+
# and end up half-migrated, with the layout edited but `config.layout`
|
|
78
|
+
# still off and nothing saying so except `ruact:doctor`.
|
|
51
79
|
def create_initializer
|
|
52
|
-
|
|
80
|
+
path = Pathname(destination_root).join("config/initializers/ruact.rb")
|
|
81
|
+
return template "initializer.rb.tt", "config/initializers/ruact.rb" unless path.exist?
|
|
82
|
+
|
|
83
|
+
inject_layout_setting(path)
|
|
53
84
|
end
|
|
54
85
|
|
|
55
86
|
def inject_controller_concern
|
|
@@ -67,21 +98,93 @@ module Ruact
|
|
|
67
98
|
after: /class ApplicationController.*\n/
|
|
68
99
|
end
|
|
69
100
|
|
|
101
|
+
# The layout owns the document: `stylesheet_link_tag`, favicons, fonts and
|
|
102
|
+
# every `<head>`-writing gem only reach a ruact page because Rails' own
|
|
103
|
+
# layout renders it (see `Ruact::Configuration#layout`). That requires TWO
|
|
104
|
+
# things in the layout — the React root, and `ruact_js_assets` to emit the
|
|
105
|
+
# bootstrap entry + this render's Flight payload. The other half of the
|
|
106
|
+
# opt-in is `config.layout = true`, which the generated initializer
|
|
107
|
+
# carries — a layout with the helper but the setting off (or the reverse)
|
|
108
|
+
# keeps rendering through ruact's built-in, CSS-less shell.
|
|
70
109
|
def inject_layout_shell
|
|
71
110
|
layout_file = "app/views/layouts/application.html.erb"
|
|
72
111
|
return unless File.exist?(Pathname(destination_root).join(layout_file))
|
|
73
112
|
|
|
74
113
|
content = File.read(Pathname(destination_root).join(layout_file))
|
|
75
|
-
|
|
76
|
-
|
|
114
|
+
|
|
115
|
+
# A CALL, not a mention: `<%# TODO: add ruact_js_assets %>` used to read
|
|
116
|
+
# as "already present" here and skip the migration, leaving the app on
|
|
117
|
+
# ruact's CSS-less shell with the generator reporting success. Shared
|
|
118
|
+
# with the runtime so both agree on what "migrated" means.
|
|
119
|
+
# BOTH halves, not just the helper. A layout carrying `ruact_js_assets`
|
|
120
|
+
# with no `<div id="root"></div>` was skipped as "already present" — and
|
|
121
|
+
# since this generator also turns `config.layout` on, that app then
|
|
122
|
+
# raised at render time on a document React could not mount into.
|
|
123
|
+
if Ruact::LayoutSource.wired?(content) && Ruact::LayoutSource.root?(content)
|
|
124
|
+
say_status "skip", "ruact root + assets already present in layout", :yellow
|
|
77
125
|
return
|
|
78
126
|
end
|
|
79
127
|
|
|
128
|
+
# Migration path for an app installed before the layout owned the
|
|
129
|
+
# document: the root is already there, only the asset call is missing.
|
|
130
|
+
#
|
|
131
|
+
# The anchor matches the ROOT DIV itself rather than the marker-then-div
|
|
132
|
+
# pair, and tolerates the ways a real layout is written — single or
|
|
133
|
+
# double quotes, extra attributes, any attribute order, CRLF, and the
|
|
134
|
+
# marker on the same line. The earlier anchor required the exact emitted
|
|
135
|
+
# formatting, so a hand-edited layout silently matched nothing. The
|
|
136
|
+
# attribute boundary in `ROOT_ELEMENT` is what keeps `data-id="root"`
|
|
137
|
+
# from being mistaken for the mount point.
|
|
138
|
+
if Ruact::LayoutSource.root?(content)
|
|
139
|
+
return migrate_layout(layout_file,
|
|
140
|
+
"\n <%= ruact_js_assets %>",
|
|
141
|
+
after: Ruact::LayoutSource::ROOT_ELEMENT,
|
|
142
|
+
success: "added ruact_js_assets to the existing layout root")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The mirror case: the helper is there but the mount target is not, so
|
|
146
|
+
# the root goes in just BEFORE the call (React needs the node in the
|
|
147
|
+
# document, and keeping the pair adjacent matches what a fresh install
|
|
148
|
+
# writes). Injecting the whole block instead would duplicate the helper.
|
|
149
|
+
if Ruact::LayoutSource.wired?(content)
|
|
150
|
+
return migrate_layout(layout_file,
|
|
151
|
+
"<%# ruact: root %>\n <div id=\"root\"></div>\n ",
|
|
152
|
+
before: Ruact::LayoutSource::ASSETS_CALL,
|
|
153
|
+
success: "added the React root div next to the existing ruact_js_assets")
|
|
154
|
+
end
|
|
155
|
+
|
|
80
156
|
inject_into_file layout_file,
|
|
81
|
-
"\n <%# ruact: root %>\n <div id=\"root\"></div>\n",
|
|
157
|
+
"\n <%# ruact: root %>\n <div id=\"root\"></div>\n <%= ruact_js_assets %>\n",
|
|
82
158
|
before: " </body>"
|
|
83
159
|
end
|
|
84
160
|
|
|
161
|
+
# `--shadcn` only. Two files, both of them things shadcn's CLI checks for
|
|
162
|
+
# and refuses to proceed without ("No Tailwind CSS configuration found" /
|
|
163
|
+
# "Could not find valid path aliases"), verified against shadcn 4.x:
|
|
164
|
+
#
|
|
165
|
+
# app/javascript/styles/globals.css — the Tailwind entry. shadcn appends
|
|
166
|
+
# its design tokens here, which is why components.json points at it.
|
|
167
|
+
# tsconfig.json — the `@/*` → `app/javascript/*` alias. The Vite plugin
|
|
168
|
+
# already registers the same alias for the BUNDLER, so components
|
|
169
|
+
# resolve at runtime today; this is what makes it resolve for
|
|
170
|
+
# TypeScript (and therefore for shadcn's alias probe and your editor).
|
|
171
|
+
#
|
|
172
|
+
# Both are guarded: an app that already has them keeps its own.
|
|
173
|
+
def create_shadcn_prerequisites
|
|
174
|
+
return unless shadcn?
|
|
175
|
+
|
|
176
|
+
create_guarded_file "app/javascript/styles/globals.css", "globals.css.tt"
|
|
177
|
+
create_guarded_file "tsconfig.json", "tsconfig.json.tt"
|
|
178
|
+
# Propshaft only serves directories that exist; the built stylesheet is
|
|
179
|
+
# generated, so the directory ships with a .keep and the artifact is
|
|
180
|
+
# gitignored (see append_gitignore_entries).
|
|
181
|
+
empty_directory "app/assets/builds"
|
|
182
|
+
create_file "app/assets/builds/.keep" unless
|
|
183
|
+
File.exist?(Pathname(destination_root).join("app/assets/builds/.keep"))
|
|
184
|
+
|
|
185
|
+
warn_unless_layout_links_builds
|
|
186
|
+
end
|
|
187
|
+
|
|
85
188
|
def create_components_directory
|
|
86
189
|
empty_directory "app/javascript/components"
|
|
87
190
|
create_file "app/javascript/components/.keep" unless
|
|
@@ -106,6 +209,10 @@ module Ruact
|
|
|
106
209
|
"app/javascript/.ruact/server-functions.ts",
|
|
107
210
|
"tmp/cache/ruact/"
|
|
108
211
|
]
|
|
212
|
+
# The compiled stylesheet is a build artifact of globals.css, rebuilt by
|
|
213
|
+
# the Procfile's `css` process on every boot — same reasoning as the
|
|
214
|
+
# generated server-functions module above.
|
|
215
|
+
entries << "app/assets/builds/tailwind.css" if shadcn?
|
|
109
216
|
# Substring matches (`existing.include?(entry)`) were unsafe — they
|
|
110
217
|
# would skip "tmp/cache/ruact/" when the file already contained
|
|
111
218
|
# "tmp/cache/ruact/some-cache.bin", leaving the directory itself
|
|
@@ -291,6 +398,8 @@ module Ruact
|
|
|
291
398
|
say " 2. Start your app: bin/dev"
|
|
292
399
|
end
|
|
293
400
|
|
|
401
|
+
show_shadcn_next_steps if shadcn?
|
|
402
|
+
|
|
294
403
|
say "\nThen add <MyComponent /> to any ERB view.\n"
|
|
295
404
|
say "Note: re-run this generator after updating the ruact gem to refresh"
|
|
296
405
|
say "the bundled Vite plugin path in vite.config.js."
|
|
@@ -299,6 +408,96 @@ module Ruact
|
|
|
299
408
|
|
|
300
409
|
private
|
|
301
410
|
|
|
411
|
+
# `inject_into_file` prints "File unchanged!" and carries on when its
|
|
412
|
+
# anchor misses, so reporting success without checking would be a lie —
|
|
413
|
+
# and the app would keep rendering through ruact's CSS-less shell with no
|
|
414
|
+
# clue why. Compare the file around the call and only claim what happened.
|
|
415
|
+
def migrate_layout(layout_file, content, success:, **anchor)
|
|
416
|
+
path = Pathname(destination_root).join(layout_file)
|
|
417
|
+
before = path.read
|
|
418
|
+
inject_into_file layout_file, content, **anchor
|
|
419
|
+
|
|
420
|
+
if path.read == before
|
|
421
|
+
warn_layout_migration_failed
|
|
422
|
+
else
|
|
423
|
+
say_status "update", success, :green
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def inject_layout_setting(path)
|
|
428
|
+
content = path.read
|
|
429
|
+
|
|
430
|
+
if content.match?(/^\s*config\.layout\s*=/)
|
|
431
|
+
say_status "skip", "config.layout already set in config/initializers/ruact.rb", :yellow
|
|
432
|
+
return
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
unless content.match?(/Ruact\.configure\s+do\s*\|(\w+)\|/)
|
|
436
|
+
warn_initializer_not_injectable
|
|
437
|
+
return
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
inject_into_file "config/initializers/ruact.rb",
|
|
441
|
+
LAYOUT_SETTING_SNIPPET,
|
|
442
|
+
after: /Ruact\.configure\s+do\s*\|\w+\|\n/
|
|
443
|
+
say_status "update", "set config.layout = true (your layout renders ruact pages)", :green
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# The compiled stylesheet still has to be REQUESTED. Rails 8's default
|
|
447
|
+
# layout links `stylesheet_link_tag :app`, which Propshaft expands over
|
|
448
|
+
# every stylesheet on the load path — so `app/assets/builds/tailwind.css`
|
|
449
|
+
# is picked up with no further wiring (verified against a generated app:
|
|
450
|
+
# the rendered `<head>` carries `/assets/tailwind-<digest>.css`).
|
|
451
|
+
#
|
|
452
|
+
# A layout that instead links stylesheets BY NAME never asks for it, and
|
|
453
|
+
# the failure is silent: Tailwind builds fine, Propshaft serves it fine,
|
|
454
|
+
# and the page is simply unstyled. Warn rather than edit — which
|
|
455
|
+
# stylesheets a layout links is the app's business.
|
|
456
|
+
def warn_unless_layout_links_builds
|
|
457
|
+
layout_path = Pathname(destination_root).join("app/views/layouts/application.html.erb")
|
|
458
|
+
return unless layout_path.exist?
|
|
459
|
+
|
|
460
|
+
content = layout_path.read
|
|
461
|
+
return unless content.include?("stylesheet_link_tag")
|
|
462
|
+
return if content.match?(/stylesheet_link_tag\s+:app\b/) || content.include?("tailwind")
|
|
463
|
+
|
|
464
|
+
say_status "notice", "your layout links stylesheets by name — add the built one:", :yellow
|
|
465
|
+
say ""
|
|
466
|
+
say " <%= stylesheet_link_tag \"tailwind\" %>"
|
|
467
|
+
say ""
|
|
468
|
+
say " `stylesheet_link_tag :app` (the Rails 8 default) would pick up"
|
|
469
|
+
say " app/assets/builds/tailwind.css on its own; a named link does not,"
|
|
470
|
+
say " and the page would render unstyled with no error."
|
|
471
|
+
say ""
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# The generator does NOT run these: `shadcn init` is interactive (it
|
|
475
|
+
# prompts for a component library and a preset) and both commands hit the
|
|
476
|
+
# network. What it CAN do is spell them exactly, which is the part nobody
|
|
477
|
+
# guesses: current shadcn defaults to **Base UI**, while ruact's generated
|
|
478
|
+
# components import **Radix** primitives — accepting the default gives you
|
|
479
|
+
# a component library the scaffold cannot use. Hence the explicit
|
|
480
|
+
# `--base radix`.
|
|
481
|
+
#
|
|
482
|
+
# The component list is ruact's own authoritative set
|
|
483
|
+
# (ScaffoldGenerator#required_shadcn_components), derived from the
|
|
484
|
+
# templates' imports, so the two generators cannot drift.
|
|
485
|
+
def show_shadcn_next_steps
|
|
486
|
+
say ""
|
|
487
|
+
say "shadcn prerequisites are in place (Tailwind entry, tsconfig alias, css process)."
|
|
488
|
+
say "Two commands remain — they are interactive and hit the network, so run them yourself:"
|
|
489
|
+
say ""
|
|
490
|
+
say " npx shadcn@latest init --base radix"
|
|
491
|
+
say " npx shadcn@latest add #{shadcn_add_list}"
|
|
492
|
+
say ""
|
|
493
|
+
say " (--base radix matters: shadcn now defaults to Base UI, but the components"
|
|
494
|
+
say " `ruact:scaffold --shadcn` generates import Radix primitives.)"
|
|
495
|
+
say " (init also asks you to pick a style preset — any of them works.)"
|
|
496
|
+
say ""
|
|
497
|
+
say "Then scaffold a resource:"
|
|
498
|
+
say " bin/rails generate ruact:scaffold Post title:string body:text --shadcn"
|
|
499
|
+
end
|
|
500
|
+
|
|
302
501
|
# Story 15.1 — the exact marker tokens delimiting the ruact-managed
|
|
303
502
|
# section of AGENTS.md. For a prose file the only safe idempotency key is
|
|
304
503
|
# an explicit marker pair (`append_gitignore_entries`-style exact-line
|
|
@@ -463,6 +662,66 @@ module Ruact
|
|
|
463
662
|
# Story 14.6 — a valid, lowercase npm "name" for the generated package.json,
|
|
464
663
|
# derived from the app directory. npm names must be lowercase and contain
|
|
465
664
|
# only URL-safe characters; anything else collapses to a hyphen.
|
|
665
|
+
# Kept verbatim in step with the `initializer.rb.tt` template's own
|
|
666
|
+
# `config.layout` block, so a migrated app and a fresh one end up reading
|
|
667
|
+
# the same thing.
|
|
668
|
+
LAYOUT_SETTING_SNIPPET = <<~RUBY
|
|
669
|
+
# Render ruact pages through this app's own layout, so the document `<head>`
|
|
670
|
+
# is yours: stylesheets, favicons, fonts and any gem that writes into
|
|
671
|
+
# `<head>` reach a ruact page. Requires the layout to call
|
|
672
|
+
# `<%= ruact_js_assets %>` (this generator adds it next to the React root).
|
|
673
|
+
config.layout = true
|
|
674
|
+
|
|
675
|
+
RUBY
|
|
676
|
+
private_constant :LAYOUT_SETTING_SNIPPET
|
|
677
|
+
|
|
678
|
+
# The initializer exists but is not the shape we know how to edit (someone
|
|
679
|
+
# rewrote it, or wrapped the configure call). Never guess at it — say what
|
|
680
|
+
# to add, so the app cannot end up half-migrated in silence.
|
|
681
|
+
def warn_initializer_not_injectable
|
|
682
|
+
say_status "skip", "could not find the Ruact.configure block to update", :red
|
|
683
|
+
say ""
|
|
684
|
+
say " Add this line inside `Ruact.configure` in config/initializers/ruact.rb:"
|
|
685
|
+
say ""
|
|
686
|
+
say " config.layout = true"
|
|
687
|
+
say ""
|
|
688
|
+
say " Without it ruact keeps using its built-in shell, which carries no"
|
|
689
|
+
say " stylesheet — your app's CSS will not reach a ruact-rendered page."
|
|
690
|
+
say ""
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
def shadcn?
|
|
694
|
+
options[:shadcn]
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
# Printed when the layout carries the ruact marker but the anchor found no
|
|
698
|
+
# root div to inject after. Silence would be the dangerous outcome: the app
|
|
699
|
+
# keeps rendering through ruact's CSS-less built-in shell, and nothing ever
|
|
700
|
+
# says why.
|
|
701
|
+
def warn_layout_migration_failed
|
|
702
|
+
say_status "skip", "could not locate the React root div in the layout", :red
|
|
703
|
+
say ""
|
|
704
|
+
say " ruact could not add `ruact_js_assets` automatically. Add it by hand,"
|
|
705
|
+
say " just after the root div in app/views/layouts/application.html.erb:"
|
|
706
|
+
say ""
|
|
707
|
+
say " <div id=\"root\"></div>"
|
|
708
|
+
say " <%= ruact_js_assets %>"
|
|
709
|
+
say ""
|
|
710
|
+
say " Without it your app's CSS cannot reach a ruact-rendered page."
|
|
711
|
+
say ""
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
# The superset the scaffold generator narrows per resource. Loaded lazily
|
|
715
|
+
# (and only under `--shadcn`) so a plain install never pays for the
|
|
716
|
+
# scaffold generator's load, and so a failure to reach it degrades to the
|
|
717
|
+
# literal list rather than aborting an otherwise-successful install.
|
|
718
|
+
def shadcn_add_list
|
|
719
|
+
require_relative "../scaffold/scaffold_shadcn_preflight"
|
|
720
|
+
ScaffoldGenerator::ShadcnPreflight::ALL_SHADCN_COMPONENTS.join(" ")
|
|
721
|
+
rescue StandardError
|
|
722
|
+
"button input textarea switch select label badge table alert-dialog dropdown-menu"
|
|
723
|
+
end
|
|
724
|
+
|
|
466
725
|
def app_package_name
|
|
467
726
|
base = File.basename(File.expand_path(destination_root))
|
|
468
727
|
sanitized = base.downcase.gsub(/[^a-z0-9._-]/, "-").squeeze("-").gsub(/\A-+|-+\z/, "")
|