ruact 0.0.9 → 0.0.11
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 +38 -1
- data/LICENSE.txt +21 -0
- data/README.md +124 -18
- data/SECURITY.md +1 -1
- data/lib/ruact/version.rb +1 -1
- data/spec/benchmarks/baseline.json +12 -1
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +17 -0
- data/spec/fixtures/readme/children-error.html.erb +3 -0
- data/spec/fixtures/readme/children-error.txt +1 -0
- data/spec/readme_demo_message_spec.rb +67 -0
- data/spec/readme_spec.rb +282 -0
- metadata +14 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 42a39cfc564acac7e1d22f07e8e1d91f143b37128a49406ecb881d2744c2ad42
|
|
4
|
+
data.tar.gz: c54b94ecdd51a1e93c37573c6c90edffab769077e965a82038a04796edbdf88a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e373a60076006eebecb968c0d6b887c05cac93c2715871df1376cd8bdf193fd01dd23e3f8317228dd350a7856da398ed92984c77a043eb7cbac051a1ef67be30
|
|
7
|
+
data.tar.gz: 2afec776569f7d7821b42df6d9d1c493648eae3b083f062f9cc0baa835a8ea26b62dbae3c4c46188e9f0021c125c1cf9c19df491275c3844461076d5c24bf47e
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.0.11] - 2026-08-25
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **The README's first sentence shows the tag instead of naming its casing convention.** It read *"Write a PascalCase tag in ERB, pass `@post` as a prop"* — which put a mechanism in the slot where the reader is still asking what they get. It also worked against the thing it was selling: for anyone arriving from React, a capitalized component tag is not a new rule, it is exactly how JSX already behaves, so naming the convention turned the familiar into a ritual to comply with. The line now shows `<LikeButton likes={@likes} />` and lets the capital letter speak for itself, with the Ruby value passed in demonstrating the no-serializer claim the sentence used to only assert. The `ERB as server components` bullet states the same mechanism one beat later, where it lands as reassurance rather than a requirement: capitalized is React, lowercase stays HTML.
|
|
15
|
+
|
|
16
|
+
The Concepts pages and the API reference are deliberately **unchanged** — a reader there is asking exactly how resolution works, and naming the convention is the right answer in that position.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **The render-pipeline allocation baseline was still the one written on the first commit.** `spec/benchmarks/baseline.json` had held April's numbers (1623 typical / 7764 heavy) since the initial commit, so the ×1.20 tolerance was being measured against a pipeline that predates route-driven codegen, contract validation and error suggestion. Four months of that work grew the typical render by 10.4% — legitimate growth, not a regression from any one change — and it finally crossed the limit. Because a single global baseline also has to absorb the ~9% spread between matrix cells (Ruby 3.4 / Rails 8.1 measures 1792 where Ruby 3.3 / Rails 7.1 measures ~1958), the overshoot surfaced as one intermittently red cell while twelve stayed green — which reads as flakiness and is not: rerunning did not fix it. Rebaselined to today's measurement, with April's numbers kept in a `_history` key rather than overwritten, and the spec now documents how to tell a regression from drift and how to regenerate the file.
|
|
21
|
+
|
|
22
|
+
## [0.0.10] - 2026-08-25
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **`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.
|
|
27
|
+
|
|
28
|
+
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`.
|
|
29
|
+
|
|
30
|
+
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.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **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.
|
|
35
|
+
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
- **`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.
|
|
39
|
+
- **`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.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **`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`.
|
|
44
|
+
|
|
10
45
|
## [0.0.9] - 2026-08-12
|
|
11
46
|
|
|
12
47
|
### Added
|
|
@@ -224,7 +259,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
224
259
|
- **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.
|
|
225
260
|
- **E2E test app** — `e2e/` Rails app (no DB, in-memory Post model) with full CRUD system tests validating the complete request cycle.
|
|
226
261
|
|
|
227
|
-
[Unreleased]: https://github.com/luizcg/ruact/compare/v0.0.
|
|
262
|
+
[Unreleased]: https://github.com/luizcg/ruact/compare/v0.0.11...HEAD
|
|
263
|
+
[0.0.11]: https://github.com/luizcg/ruact/releases/tag/v0.0.11
|
|
264
|
+
[0.0.10]: https://github.com/luizcg/ruact/releases/tag/v0.0.10
|
|
228
265
|
[0.0.9]: https://github.com/luizcg/ruact/releases/tag/v0.0.9
|
|
229
266
|
[0.0.8]: https://github.com/luizcg/ruact/releases/tag/v0.0.8
|
|
230
267
|
[0.0.7]: https://github.com/luizcg/ruact/releases/tag/v0.0.7
|
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 `<LikeButton likes={@likes} />` in an ERB template and a React component renders, with a Ruby value passed straight in — 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 use your components by name in the views you already have: capitalized is React, lowercase stays HTML. [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
|
|
data/lib/ruact/version.rb
CHANGED
|
@@ -1 +1,12 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"typical_allocations": 1792,
|
|
3
|
+
"heavy_allocations": 8669,
|
|
4
|
+
"_measured_on": "2026-08-25, Ruby 3.4.5 / Rails 8.1, gem at v0.0.10",
|
|
5
|
+
"_history": {
|
|
6
|
+
"2026-04-19": {
|
|
7
|
+
"typical_allocations": 1623,
|
|
8
|
+
"heavy_allocations": 7764,
|
|
9
|
+
"note": "Original baseline, written by the spec's first run at the initial commit. Superseded 2026-08-25: four months of pipeline work (route-driven codegen, contract validation, error suggestion) grew the typical render past the 20% tolerance measured against April's code. The overshoot was cumulative and legitimate, not a regression from any single change; it surfaced as an intermittently red matrix cell because ONE global baseline cannot absorb the ~9% spread between Ruby/Rails combinations (Ruby 3.3 / Rails 7.1 measured ~1958 while Ruby 3.4 / Rails 8.1 measured 1792). Growth since April: typical +10.4%, heavy +11.7%."
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -7,6 +7,23 @@ require "json"
|
|
|
7
7
|
|
|
8
8
|
BENCHMARK_BASELINE_FILE = File.expand_path("baseline.json", __dir__)
|
|
9
9
|
|
|
10
|
+
# The baseline is a SINGLE measurement shared by every matrix cell, while the
|
|
11
|
+
# same render allocates differently per Ruby/Rails combination — measured at
|
|
12
|
+
# ~9% spread (Ruby 3.4/Rails 8.1: 1792; Ruby 3.3/Rails 7.1: ~1958). The x1.20
|
|
13
|
+
# tolerance therefore absorbs two different things at once: that cross-version
|
|
14
|
+
# spread AND real growth. When the two together cross the limit, ONE cell goes
|
|
15
|
+
# red while the rest stay green, which reads like flakiness and is not — rerunning
|
|
16
|
+
# does not fix it.
|
|
17
|
+
#
|
|
18
|
+
# Before rebaselining, check WHICH it is. A number that moved with a code change
|
|
19
|
+
# is a regression and belongs in the diff, not in this file. A number that drifted
|
|
20
|
+
# across many changes has outgrown its anchor, and the honest move is to rebaseline
|
|
21
|
+
# and record the growth in baseline.json's `_history` (see the 2026-08-25 entry).
|
|
22
|
+
#
|
|
23
|
+
# To regenerate: delete baseline.json and run this file TWICE (the first run writes
|
|
24
|
+
# `typical_allocations`, the second fills `heavy_allocations`), then restore the
|
|
25
|
+
# `_measured_on` / `_history` keys, which the spec does not write.
|
|
26
|
+
|
|
10
27
|
RSpec.describe "RenderPipeline benchmark" do
|
|
11
28
|
include RSpec::Benchmark::Matchers
|
|
12
29
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruact: <LikeButton> at app/views/home/index.html.erb:3 children are not supported — pass content as a prop, e.g. `<LikeButton content={...} />`.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
# Story 5.2 — the anti-rot gate for the README's write→verify demo.
|
|
7
|
+
#
|
|
8
|
+
# ⚠️ THE README DEMO SHOWS THIS MESSAGE. If this spec goes red, the recording
|
|
9
|
+
# at https://ruact.dev/readme-write-verify.gif is now showing a message the
|
|
10
|
+
# gem no longer produces — RE-RECORD IT.
|
|
11
|
+
# Sources: design-artifacts/E-Assets/readme-write-verify/ in the planning
|
|
12
|
+
# repository (`./build-app.sh && ./record.sh && ./assemble.sh`).
|
|
13
|
+
#
|
|
14
|
+
# The fixture is PRODUCED BY THE GEM, never transcribed. It was written by
|
|
15
|
+
# running the preprocessor over the committed `.html.erb` beside it:
|
|
16
|
+
#
|
|
17
|
+
# ruby -Ilib -rruact -e 'begin
|
|
18
|
+
# Ruact::ErbPreprocessor.transform(
|
|
19
|
+
# File.read("spec/fixtures/readme/children-error.html.erb"),
|
|
20
|
+
# identifier: "app/views/home/index.html.erb")
|
|
21
|
+
# rescue Ruact::ChildrenNotSupportedError => e
|
|
22
|
+
# File.write("spec/fixtures/readme/children-error.txt", e.message + "\n")
|
|
23
|
+
# end'
|
|
24
|
+
#
|
|
25
|
+
# The identifier is the guide's own template path, so the fixture is verbatim
|
|
26
|
+
# the line the recording shows (modulo the absolute prefix Rails prepends in a
|
|
27
|
+
# real app). The monorepo owns the other half of this gate: the same failure
|
|
28
|
+
# reached through a real Rails render, in
|
|
29
|
+
# docs/examples/getting-started/harness/spec/requests/getting_started_spec.rb.
|
|
30
|
+
# That one cannot live here — the gem repository is public and reads nothing
|
|
31
|
+
# from the private planning repository — which is exactly why this spec exists:
|
|
32
|
+
# the published artifact stays verifiable in its own repository.
|
|
33
|
+
RSpec.describe "the README demo's error message", :story_5_2 do
|
|
34
|
+
let(:root) { Pathname.new(File.expand_path("..", __dir__)) }
|
|
35
|
+
let(:fixtures) { root.join("spec/fixtures/readme") }
|
|
36
|
+
let(:source) { fixtures.join("children-error.html.erb").read }
|
|
37
|
+
let(:expected) { fixtures.join("children-error.txt").read.strip }
|
|
38
|
+
|
|
39
|
+
# The exact call the recording's third beat makes, one layer down: a template
|
|
40
|
+
# whose PascalCase tag has children, compiled with the guide's template path.
|
|
41
|
+
def message
|
|
42
|
+
Ruact::ErbPreprocessor.transform(source, identifier: "app/views/home/index.html.erb")
|
|
43
|
+
raise "expected Ruact::ChildrenNotSupportedError, none was raised"
|
|
44
|
+
rescue Ruact::ChildrenNotSupportedError => e
|
|
45
|
+
e.message
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "is byte-for-byte what the gem produces today" do
|
|
49
|
+
expect(message).to eq(expected),
|
|
50
|
+
"the loud-children message changed. The README demo at " \
|
|
51
|
+
"https://ruact.dev/readme-write-verify.gif shows the old one — " \
|
|
52
|
+
"regenerate the fixture, then RE-RECORD the demo."
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "names the component, the file:line and the fix — the three things the demo is about" do
|
|
56
|
+
expect(expected).to include("<LikeButton>")
|
|
57
|
+
expect(expected).to include("app/views/home/index.html.erb:3")
|
|
58
|
+
expect(expected).to include("<LikeButton content={...} />")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "is the message the README's demo is claimed to show" do
|
|
62
|
+
readme = root.join("README.md").read
|
|
63
|
+
|
|
64
|
+
expect(readme).to include("https://ruact.dev/readme-write-verify.gif")
|
|
65
|
+
expect(readme).to include("Ruact::ChildrenNotSupportedError")
|
|
66
|
+
end
|
|
67
|
+
end
|
data/spec/readme_spec.rb
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "nokogiri"
|
|
5
|
+
require "pathname"
|
|
6
|
+
|
|
7
|
+
# Story 5.14 — the README gate that lives INSIDE this repository.
|
|
8
|
+
#
|
|
9
|
+
# `README.md` is not an ordinary readme: GitHub renders it on the gem's own
|
|
10
|
+
# repository page, `source_code_uri` points at it, and `spec.files` packages it
|
|
11
|
+
# *inside* the built `.gem`. Until this story it was still `bundle gem`
|
|
12
|
+
# boilerplate ("TODO: Delete this and the text below"), and that boilerplate had
|
|
13
|
+
# already shipped to RubyGems.
|
|
14
|
+
#
|
|
15
|
+
# The planning monorepo has a command-spine gate
|
|
16
|
+
# (`docs/examples/getting-started/scripts/check-commands.mjs`) that reads this
|
|
17
|
+
# file through the `gem/` submodule and checks its quick start against ONE
|
|
18
|
+
# canonical greenfield sequence — the SPINE, which is the single source of
|
|
19
|
+
# truth. But a merge in THIS repository fires nothing over there, so a
|
|
20
|
+
# README-only PR here would stay unchecked until somebody bumped the submodule
|
|
21
|
+
# pointer.
|
|
22
|
+
#
|
|
23
|
+
# Hence the deliberate duplication below: `expected_quick_start` is a literal
|
|
24
|
+
# copy of the quick-start block, owned by this spec, and the monorepo gate
|
|
25
|
+
# verifies that literal against the canonical SPINE. The dependency runs one way
|
|
26
|
+
# only — this spec reads nothing outside the gem repository, because a public
|
|
27
|
+
# artifact that cannot be verified in its own repository is not verifiable.
|
|
28
|
+
RSpec.describe "README.md", :story_5_14 do
|
|
29
|
+
subject(:readme) { root.join("README.md").read }
|
|
30
|
+
|
|
31
|
+
let(:root) { Pathname.new(File.expand_path("..", __dir__)) }
|
|
32
|
+
|
|
33
|
+
# Every literal `bundle gem` leaves behind. Each one was present before this
|
|
34
|
+
# story; any of them coming back means the scaffolding was re-pasted.
|
|
35
|
+
let(:boilerplate) do
|
|
36
|
+
[
|
|
37
|
+
"TODO:",
|
|
38
|
+
"UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG",
|
|
39
|
+
"[USERNAME]",
|
|
40
|
+
"Welcome to your new gem",
|
|
41
|
+
"Put your Ruby code in the file",
|
|
42
|
+
"bin/console",
|
|
43
|
+
"bundle exec rake release"
|
|
44
|
+
]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Harness-only gem sources. They exist so the monorepo's playgrounds can test
|
|
48
|
+
# the working tree; a reader who copies one installs nothing.
|
|
49
|
+
let(:path_gem_sources) do
|
|
50
|
+
[
|
|
51
|
+
/(^|\s)gem\s+["']ruact["']\s*,\s*path:/,
|
|
52
|
+
/--path(\s|=)/
|
|
53
|
+
]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The quick-start block, byte for byte. Changing it here without changing the
|
|
57
|
+
# monorepo SPINE (or the other way round) turns the monorepo gate red.
|
|
58
|
+
let(:expected_quick_start) do
|
|
59
|
+
<<~BASH
|
|
60
|
+
# 1. A throwaway app to try it in
|
|
61
|
+
rails new myapp --skip-javascript && cd myapp
|
|
62
|
+
|
|
63
|
+
# 2. Add the gem
|
|
64
|
+
bundle add ruact
|
|
65
|
+
|
|
66
|
+
# 3. Write the config, the layout wiring and an AGENTS.md — then run npm install
|
|
67
|
+
rails generate ruact:install
|
|
68
|
+
|
|
69
|
+
# 4. Rails + Vite, one command
|
|
70
|
+
bin/dev
|
|
71
|
+
BASH
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# `[text](target)` and ``, inline-title form allowed.
|
|
75
|
+
def markdown_link_targets(markdown)
|
|
76
|
+
markdown.scan(/\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/).flatten
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Story 5.2 — the demo reference, pinned BYTE FOR BYTE.
|
|
80
|
+
#
|
|
81
|
+
# The first version of this gate hand-rolled HTML and CommonMark recognition
|
|
82
|
+
# out of regexes, and review corrected it in three consecutive rounds:
|
|
83
|
+
# `\bsrc=` matched `data-src=`, a literal `>` inside an attribute ended the
|
|
84
|
+
# tag early, fences were only recognised at column 0, then unterminated
|
|
85
|
+
# fences were not recognised at all, then unquoted attribute values were
|
|
86
|
+
# invisible. Those are not five bugs. Neither HTML nor CommonMark is a
|
|
87
|
+
# regular language, so that list has no end, and a gate whose coverage is
|
|
88
|
+
# "whatever the regex happens to know this week" is not a gate.
|
|
89
|
+
#
|
|
90
|
+
# Replaced by three statements that are TOTAL:
|
|
91
|
+
#
|
|
92
|
+
# 1. the reference is pinned literally — the same move `expected_quick_start`
|
|
93
|
+
# above already makes for the quick start;
|
|
94
|
+
# 2. the file is allowed EXACTLY ONE raw `<img>`, counted at the byte level;
|
|
95
|
+
# and
|
|
96
|
+
# 3. the parsed document holds exactly one LIVE `<img>` element, equal to
|
|
97
|
+
# the pinned one attribute for attribute.
|
|
98
|
+
#
|
|
99
|
+
# (1) and (2) together say something the old scanner could only approximate:
|
|
100
|
+
# every raw image reference in this README is the pinned one. A second `<img>`
|
|
101
|
+
# anywhere — including inside a fenced example, terminated or not — turns
|
|
102
|
+
# this red and has to be gated deliberately rather than slipping through a
|
|
103
|
+
# blind spot. (3) says the thing bytes cannot: that it renders. The pinned
|
|
104
|
+
# block wrapped in an HTML comment satisfies (1) and (2) and shows nothing.
|
|
105
|
+
#
|
|
106
|
+
# Attributes are then read with Nokogiri, already a runtime dependency of this
|
|
107
|
+
# gem (see the gemspec), so unquoted values, `>` inside a value and `data-src`
|
|
108
|
+
# are PARSED rather than pattern-matched. What that scope does and does not
|
|
109
|
+
# promise is written down in the monorepo's gate inventory,
|
|
110
|
+
# docs/examples/getting-started/README.md § "What is still outside both gates".
|
|
111
|
+
def expected_demo_img
|
|
112
|
+
<<~HTML
|
|
113
|
+
<img src="https://ruact.dev/readme-write-verify.gif" width="800"
|
|
114
|
+
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." />
|
|
115
|
+
HTML
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def demo_src
|
|
119
|
+
"https://ruact.dev/readme-write-verify.gif"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Parsed from the pinned literal, exactly as `expected_quick_start` is the
|
|
123
|
+
# authority for the quick start: one example asserts the README contains
|
|
124
|
+
# these bytes, and the rest read meaning off them. The chain is
|
|
125
|
+
# README -> literal -> parser, and the first link is what makes it honest.
|
|
126
|
+
def demo_node
|
|
127
|
+
Nokogiri::HTML5.fragment(expected_demo_img).at_css("img")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Byte-level and case-insensitive on purpose: no parser, no markdown, nothing
|
|
131
|
+
# to have a blind spot. `<IMG`, `<img\n`, an `<img` inside a fence — all count.
|
|
132
|
+
def raw_img_count(markdown)
|
|
133
|
+
markdown.scan(/<img\b/i).length
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# …and the same parser turned on the README ITSELF, because the two byte-level
|
|
137
|
+
# facts above are about bytes, not about rendering: wrap the pinned block in
|
|
138
|
+
# an HTML comment and the literal is still present and the raw count is still
|
|
139
|
+
# one, while GitHub shows nothing at all (verified — `include`=true, raw=1,
|
|
140
|
+
# live nodes=0). Nokogiri is what decides whether a tag is an element or a
|
|
141
|
+
# comment, so it is what the "it renders" half of this gate has to ask.
|
|
142
|
+
def readme_img_nodes
|
|
143
|
+
Nokogiri::HTML5.fragment(readme).css("img")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Absolute URLs, anchors and mailto: links are somebody else's problem; on-disk
|
|
147
|
+
# targets are ours.
|
|
148
|
+
def relative(targets)
|
|
149
|
+
targets.reject { |target| target.start_with?("http://", "https://", "#", "mailto:") }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def bash_blocks(markdown)
|
|
153
|
+
markdown.scan(/^```bash[ \t]*\n(.*?)^```/m).flatten
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "carries none of the `bundle gem` boilerplate" do
|
|
157
|
+
present = boilerplate.select { |literal| readme.include?(literal) }
|
|
158
|
+
|
|
159
|
+
expect(present).to be_empty,
|
|
160
|
+
"README.md still carries bundler scaffolding: #{present.inspect}"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it "links only to files that exist in this repository" do
|
|
164
|
+
targets = relative(markdown_link_targets(readme) + [demo_node["src"]])
|
|
165
|
+
missing = targets.reject { |target| root.join(target.split("#").first.to_s).exist? }
|
|
166
|
+
|
|
167
|
+
expect(targets).not_to be_empty, "expected the README to link at least one repo-relative file"
|
|
168
|
+
expect(missing).to be_empty,
|
|
169
|
+
"README.md links files that do not exist in the gem repository: #{missing.inspect}"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Story 5.2 — the demo GIF is a documentation asset hosted with the site, so
|
|
173
|
+
# this repository stays free of binaries: no `.gem` download and no clone
|
|
174
|
+
# pays for it. The consequence is that the reference is an absolute URL, and
|
|
175
|
+
# an absolute URL cannot be resolved on disk here — the monorepo puts it in
|
|
176
|
+
# `website/scripts/verify-urls.mjs`'s PATHS, checked against the deployed
|
|
177
|
+
# site, so a dead image goes red there instead of rotting silently.
|
|
178
|
+
it "carries the write→verify demo reference, byte for byte" do
|
|
179
|
+
expect(readme).to include(expected_demo_img),
|
|
180
|
+
"the demo reference in README.md drifted from the literal this spec pins. " \
|
|
181
|
+
"Change both or neither — and if the recording itself changed, see " \
|
|
182
|
+
"readme_demo_message_spec.rb."
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# The other half of the pin: because there is exactly ONE raw `<img>` and the
|
|
186
|
+
# example above proves it is the pinned one, "the pinned literal is present"
|
|
187
|
+
# and "every raw image in this file is checked" are the same statement.
|
|
188
|
+
it "carries exactly one raw <img>, so the pin covers every raw image in the file" do
|
|
189
|
+
expect(raw_img_count(readme)).to eq(1),
|
|
190
|
+
"README.md has #{raw_img_count(readme)} raw <img> tags. This gate is " \
|
|
191
|
+
"written for exactly one — the demo, pinned literally. A second image " \
|
|
192
|
+
"must be gated deliberately, not left to a scanner's blind spots."
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# And the demo is LIVE, not merely present. Bytes inside an HTML comment
|
|
196
|
+
# satisfy both statements above and render nothing; only a parser can tell
|
|
197
|
+
# the difference, so the README itself is parsed and the node it yields must
|
|
198
|
+
# be the pinned one, attribute for attribute.
|
|
199
|
+
it "renders the demo as a real element, not as bytes inside a comment" do
|
|
200
|
+
nodes = readme_img_nodes
|
|
201
|
+
|
|
202
|
+
expect(nodes.length).to eq(1),
|
|
203
|
+
"README.md parses to #{nodes.length} live <img> element(s); the demo must be " \
|
|
204
|
+
"exactly one, and must not be commented out."
|
|
205
|
+
expect(nodes.first.attributes.transform_values(&:value))
|
|
206
|
+
.to eq(demo_node.attributes.transform_values(&:value))
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it "keeps the demo hosted with the site rather than committed here" do
|
|
210
|
+
expect(demo_node["src"]).to eq(demo_src)
|
|
211
|
+
|
|
212
|
+
# `git ls-files` and not a filesystem glob, because that is precisely what
|
|
213
|
+
# `spec.files` packages (ruact.gemspec) — untracked build output is not the
|
|
214
|
+
# question here, and coverage/ is full of it.
|
|
215
|
+
tracked = IO.popen(%w[git ls-files -z], chdir: root.to_s, err: IO::NULL) do |ls|
|
|
216
|
+
ls.readlines("\x0", chomp: true)
|
|
217
|
+
end
|
|
218
|
+
committed_media = tracked.grep(/\.(gif|mp4|webm|webp)\z/i)
|
|
219
|
+
|
|
220
|
+
expect(committed_media).to be_empty,
|
|
221
|
+
"media committed under gem/ ships inside every `.gem` and stays in the " \
|
|
222
|
+
"clone history forever: #{committed_media.inspect}"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# The alt text is the only thing a screen-reader user — or an agent reading
|
|
226
|
+
# the README as text — gets. "demo" is not a description.
|
|
227
|
+
it "describes the demo's arc in its alt text" do
|
|
228
|
+
# Nokogiri resolves the entities, so this reads what a screen reader reads:
|
|
229
|
+
# `<LikeButton` is the tag the demo shows, not four literal characters.
|
|
230
|
+
alt = demo_node["alt"].to_s
|
|
231
|
+
|
|
232
|
+
expect(alt.split.length).to be > 40, "the demo's alt text does not describe the arc: #{alt.inspect}"
|
|
233
|
+
expect(alt).to include("Ruact::ChildrenNotSupportedError")
|
|
234
|
+
expect(alt).not_to match(/\bbuild\b/i),
|
|
235
|
+
"the failure the demo shows happens server-side at render, not at build — " \
|
|
236
|
+
"see Story 5.2 AC4"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Story 5.2, learned the expensive way: YARD parses README.md as the docs'
|
|
240
|
+
# main file and read `{@likes}` in the demo's alt text as a link macro it
|
|
241
|
+
# could not resolve — and `--fail-on-warning` turned that into a red REQUIRED
|
|
242
|
+
# check, for a README edit, in a job whose output says nothing about READMEs.
|
|
243
|
+
# Braces belong in prose as `{`/`}`: GitHub renders them, YARD never
|
|
244
|
+
# sees them.
|
|
245
|
+
#
|
|
246
|
+
# SCOPE, deliberately: this checks the pinned demo block, not the whole file.
|
|
247
|
+
# Knowing which parts of a markdown document YARD linkifies means knowing
|
|
248
|
+
# where the fenced blocks are, and this gate no longer recognises fences —
|
|
249
|
+
# that is the trade this redesign makes. `yard --fail-on-warning` in this
|
|
250
|
+
# repository's own CI is the TOTAL gate; this example exists so the one line
|
|
251
|
+
# that actually tripped it fails locally, in the suite that owns the README,
|
|
252
|
+
# naming the cause.
|
|
253
|
+
it "keeps YARD link macros out of the demo block, which is what reddened the docs job" do
|
|
254
|
+
# `{@ivar}`, `{Class}`, `{Class::Nested}`, `{Class#method}`, `{Class.method}`
|
|
255
|
+
# — the shapes YARD resolves. It does not try to resolve anything with a
|
|
256
|
+
# space in it, which is why the prose's `{ post: … }` elsewhere is safe.
|
|
257
|
+
macros = expected_demo_img.scan(/\{(?:@\w+|[A-Z][\w:.#]*)\}/)
|
|
258
|
+
|
|
259
|
+
expect(macros).to be_empty,
|
|
260
|
+
"the demo block contains #{macros.inspect}, which YARD tries to resolve as a " \
|
|
261
|
+
"link and `yard --fail-on-warning` fails on. Use { / }."
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
it "shows no harness-only `path:` gem source a reader could copy" do
|
|
265
|
+
offenders = path_gem_sources.filter_map { |pattern| pattern.source if readme.match?(pattern) }
|
|
266
|
+
|
|
267
|
+
expect(offenders).to be_empty,
|
|
268
|
+
"README.md shows a harness-only gem source (#{offenders.inspect}) — " \
|
|
269
|
+
"a reader who copies it installs nothing"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it "pins the quick start to the canonical greenfield sequence" do
|
|
273
|
+
expect(bash_blocks(readme).first).to eq(expected_quick_start),
|
|
274
|
+
"the README quick start drifted. It is checked against the monorepo's " \
|
|
275
|
+
"canonical SPINE (docs/examples/getting-started/scripts/" \
|
|
276
|
+
"check-commands.mjs); change both or neither."
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it "keeps the quick start in a ```bash fence, the only language the monorepo gate reads" do
|
|
280
|
+
expect(readme).to include("```bash\n#{expected_quick_start}```")
|
|
281
|
+
end
|
|
282
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruact
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Luiz Garcia
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: nokogiri
|
|
@@ -24,7 +24,12 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '1.15'
|
|
27
|
-
description:
|
|
27
|
+
description: |
|
|
28
|
+
ruact renders React components straight from your ERB views: write a PascalCase
|
|
29
|
+
tag, pass a Ruby value as a prop, and React hydrates it in the browser over the
|
|
30
|
+
Flight wire format. Server functions and queries are drawn from your existing
|
|
31
|
+
route table, so there is no hand-written JSON layer to keep in sync and no Node
|
|
32
|
+
process in production.
|
|
28
33
|
email:
|
|
29
34
|
- luizcg@gmail.com
|
|
30
35
|
executables: []
|
|
@@ -37,6 +42,7 @@ files:
|
|
|
37
42
|
- ".rubocop.yml"
|
|
38
43
|
- ".rubocop_todo.yml"
|
|
39
44
|
- CHANGELOG.md
|
|
45
|
+
- LICENSE.txt
|
|
40
46
|
- README.md
|
|
41
47
|
- RELEASING.md
|
|
42
48
|
- Rakefile
|
|
@@ -156,6 +162,8 @@ files:
|
|
|
156
162
|
- spec/fixtures/flight/string_basic.txt
|
|
157
163
|
- spec/fixtures/flight/string_dollar_escape.txt
|
|
158
164
|
- spec/fixtures/flight/undefined.txt
|
|
165
|
+
- spec/fixtures/readme/children-error.html.erb
|
|
166
|
+
- spec/fixtures/readme/children-error.txt
|
|
159
167
|
- spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb
|
|
160
168
|
- spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb
|
|
161
169
|
- spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb
|
|
@@ -167,6 +175,8 @@ files:
|
|
|
167
175
|
- spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb
|
|
168
176
|
- spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb
|
|
169
177
|
- spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb
|
|
178
|
+
- spec/readme_demo_message_spec.rb
|
|
179
|
+
- spec/readme_spec.rb
|
|
170
180
|
- spec/ruact/client_manifest_spec.rb
|
|
171
181
|
- spec/ruact/component_contract_spec.rb
|
|
172
182
|
- spec/ruact/configuration_spec.rb
|
|
@@ -265,6 +275,7 @@ metadata:
|
|
|
265
275
|
source_code_uri: https://github.com/luizcg/ruact
|
|
266
276
|
changelog_uri: https://github.com/luizcg/ruact/blob/main/CHANGELOG.md
|
|
267
277
|
bug_tracker_uri: https://github.com/luizcg/ruact/issues
|
|
278
|
+
documentation_uri: https://ruact.dev
|
|
268
279
|
rubygems_mfa_required: 'true'
|
|
269
280
|
post_install_message:
|
|
270
281
|
rdoc_options: []
|