ruact 0.0.11 → 0.0.12
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 +110 -48
- data/CONTRIBUTING.md +233 -0
- data/README.md +14 -4
- data/RELEASING.md +125 -139
- data/lib/ruact/packaging.rb +68 -0
- data/lib/ruact/version.rb +1 -1
- data/vendor/javascript/vite-plugin-ruact/flight-client.test.mjs +321 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +11 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +1 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +14 -3
- metadata +6 -107
- data/.codecov.yml +0 -31
- data/.github/workflows/ci.yml +0 -284
- data/.github/workflows/server-functions-bench.yml +0 -54
- data/.rubocop.yml +0 -107
- data/.rubocop_todo.yml +0 -63
- data/Rakefile +0 -10
- data/bench/server_functions_dispatch_bench.rb +0 -276
- data/bench/server_functions_dispatch_bench.results.md +0 -150
- data/docs/internal/README.md +0 -9
- data/docs/internal/decisions/server-functions-api.md +0 -2236
- data/spec/benchmarks/baseline.json +0 -12
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -109
- data/spec/fixtures/flight/README.md +0 -136
- data/spec/fixtures/flight/array.txt +0 -1
- data/spec/fixtures/flight/as_json_object.txt +0 -2
- data/spec/fixtures/flight/bigint.txt +0 -1
- data/spec/fixtures/flight/boolean_false.txt +0 -1
- data/spec/fixtures/flight/boolean_true.txt +0 -1
- data/spec/fixtures/flight/client_component_with_props.txt +0 -2
- data/spec/fixtures/flight/client_reference.txt +0 -2
- data/spec/fixtures/flight/hash.txt +0 -1
- data/spec/fixtures/flight/infinity.txt +0 -1
- data/spec/fixtures/flight/nan.txt +0 -1
- data/spec/fixtures/flight/negative_infinity.txt +0 -1
- data/spec/fixtures/flight/nil.txt +0 -1
- data/spec/fixtures/flight/number_float.txt +0 -1
- data/spec/fixtures/flight/number_integer.txt +0 -1
- data/spec/fixtures/flight/react_element_no_props.txt +0 -1
- data/spec/fixtures/flight/redirect_row.txt +0 -1
- data/spec/fixtures/flight/serializable_object.txt +0 -2
- data/spec/fixtures/flight/string_basic.txt +0 -1
- data/spec/fixtures/flight/string_dollar_escape.txt +0 -1
- data/spec/fixtures/flight/undefined.txt +0 -1
- data/spec/fixtures/readme/children-error.html.erb +0 -3
- data/spec/fixtures/readme/children-error.txt +0 -1
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +0 -16
- data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +0 -24
- data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +0 -15
- data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +0 -17
- data/spec/readme_demo_message_spec.rb +0 -67
- data/spec/readme_spec.rb +0 -282
- data/spec/ruact/client_manifest_spec.rb +0 -270
- data/spec/ruact/component_contract_spec.rb +0 -119
- data/spec/ruact/configuration_spec.rb +0 -518
- data/spec/ruact/controller_request_spec.rb +0 -671
- data/spec/ruact/controller_spec.rb +0 -343
- data/spec/ruact/doctor_spec.rb +0 -769
- data/spec/ruact/erb_preprocessor_hook_spec.rb +0 -55
- data/spec/ruact/erb_preprocessor_spec.rb +0 -361
- data/spec/ruact/errors_spec.rb +0 -93
- data/spec/ruact/flight/renderer_spec.rb +0 -133
- data/spec/ruact/flight/serializer_spec.rb +0 -494
- data/spec/ruact/html_converter_spec.rb +0 -375
- data/spec/ruact/install_generator_spec.rb +0 -1549
- data/spec/ruact/layout_source_spec.rb +0 -108
- data/spec/ruact/manifest_resolver_spec.rb +0 -174
- data/spec/ruact/query_request_spec.rb +0 -706
- data/spec/ruact/query_spec.rb +0 -105
- data/spec/ruact/railtie_spec.rb +0 -155
- data/spec/ruact/render_context_spec.rb +0 -58
- data/spec/ruact/render_pipeline_concurrency_spec.rb +0 -78
- data/spec/ruact/render_pipeline_spec.rb +0 -928
- data/spec/ruact/scaffold_generator_spec.rb +0 -1849
- data/spec/ruact/serializable_spec.rb +0 -179
- data/spec/ruact/server_bucket_request_spec.rb +0 -785
- data/spec/ruact/server_function_name_spec.rb +0 -53
- data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +0 -63
- data/spec/ruact/server_functions/bucket_two_payload_spec.rb +0 -200
- data/spec/ruact/server_functions/codegen_spec.rb +0 -397
- data/spec/ruact/server_functions/error_payload_spec.rb +0 -222
- data/spec/ruact/server_functions/error_suggestion_spec.rb +0 -79
- data/spec/ruact/server_functions/introspection_spec.rb +0 -135
- data/spec/ruact/server_functions/name_bridge_spec.rb +0 -212
- data/spec/ruact/server_functions/query_context_spec.rb +0 -72
- data/spec/ruact/server_functions/query_source_spec.rb +0 -193
- data/spec/ruact/server_functions/railtie_integration_spec.rb +0 -215
- data/spec/ruact/server_functions/rake_spec.rb +0 -86
- data/spec/ruact/server_functions/route_source_spec.rb +0 -202
- data/spec/ruact/server_functions/snapshot_spec.rb +0 -96
- data/spec/ruact/server_functions/snapshot_writer_spec.rb +0 -71
- data/spec/ruact/server_rescue_request_spec.rb +0 -416
- data/spec/ruact/server_spec.rb +0 -179
- data/spec/ruact/server_upload_request_spec.rb +0 -311
- data/spec/ruact/signed_references_spec.rb +0 -164
- data/spec/ruact/string_distance_spec.rb +0 -38
- data/spec/ruact/tasks_json_introspection_spec.rb +0 -141
- data/spec/ruact/testing/have_ruact_component_spec.rb +0 -170
- data/spec/ruact/testing/no_production_load_spec.rb +0 -41
- data/spec/ruact/validation_errors_spec.rb +0 -116
- data/spec/ruact/view_helper_spec.rb +0 -131
- data/spec/spec_helper.rb +0 -77
- data/spec/support/fixtures/pixel.png +0 -0
- data/spec/support/flight_wire_parser.rb +0 -21
- data/spec/support/flight_wire_parser_spec.rb +0 -93
- data/spec/support/matchers/flight_fixture_matcher.rb +0 -130
- data/spec/support/matchers/flight_fixture_matcher_spec.rb +0 -250
- data/spec/support/rails_stub.rb +0 -115
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Contributing to ruact
|
|
2
|
+
|
|
3
|
+
Thanks for wanting to change something here. This guide takes you from a fresh clone to an open pull
|
|
4
|
+
request, and everything it asks you to run, you can run — with this repository and nothing else.
|
|
5
|
+
|
|
6
|
+
## What this repository is
|
|
7
|
+
|
|
8
|
+
`luizcg/ruact` is one repository holding three things that ship together:
|
|
9
|
+
|
|
10
|
+
| Directory | What lives there |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `lib/` | the Ruby library — the ERB preprocessor, the render pipeline, the Flight wire format, the Rails integration, the generators and the Rake tasks |
|
|
13
|
+
| `vendor/javascript/vite-plugin-ruact/` | the Vite plugin — it scans for `"use client"`, emits the client manifest, and generates the typed server-function accessors |
|
|
14
|
+
| `vendor/javascript/ruact-server-functions-runtime/` | the browser-side runtime those generated accessors call |
|
|
15
|
+
|
|
16
|
+
The two JavaScript packages are **bundled, not published**. A host app never installs them from npm:
|
|
17
|
+
`rails generate ruact:install` writes a `vite.config.js` that imports the plugin by absolute path off the
|
|
18
|
+
installed gem. That has one consequence worth stating up front, because it is what makes the rest of this
|
|
19
|
+
document short — **pointing a Rails app at your checkout of the gem points it at your checkout of the Vite
|
|
20
|
+
plugin too.** There is nothing to link separately, and nothing else to clone. This repository has no
|
|
21
|
+
submodules.
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
| Tool | Version | Why |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| Ruby | >= 3.2 | the floor in `ruact.gemspec`; CI runs 3.2 and 3.3 |
|
|
28
|
+
| Node.js | 20 | the JavaScript suite and the asset build only — ruact runs no Node process in production |
|
|
29
|
+
| Bundler | any recent | `gem install bundler` |
|
|
30
|
+
|
|
31
|
+
You do not need a Rails **app** to work on this gem, and you do not install Rails by hand: the `Gemfile`
|
|
32
|
+
pins it to `RAILS_VERSION`, defaulting to 8.0, so `bundle install` brings it. What keeps the suite fast is
|
|
33
|
+
that `spec/support/rails_stub.rb` loads only Rails' core and adds test-only writers on top — the request
|
|
34
|
+
cycle (`action_controller`, `action_view`) is loaded by the handful of specs that actually need it, and by
|
|
35
|
+
nothing else.
|
|
36
|
+
|
|
37
|
+
## Setup
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/luizcg/ruact.git
|
|
41
|
+
cd ruact
|
|
42
|
+
bundle install
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
That is the whole setup for a Ruby-side change. If you are touching the Vite plugin or the browser
|
|
46
|
+
runtime, install their dependencies too:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cd vendor/javascript/ruact-server-functions-runtime && npm install
|
|
50
|
+
cd ../vite-plugin-ruact && npm ci
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The first install is not decoration: the plugin's test run reaches into the runtime package, which keeps
|
|
54
|
+
`react` in its own dependencies.
|
|
55
|
+
|
|
56
|
+
## Running the checks
|
|
57
|
+
|
|
58
|
+
These are the jobs in [`.github/workflows/ci.yml`](.github/workflows/ci.yml), which runs on every push and
|
|
59
|
+
every pull request. Running them locally first is the difference between one round and four.
|
|
60
|
+
|
|
61
|
+
<!-- ci-jobs:begin -->
|
|
62
|
+
|
|
63
|
+
| CI job | What it checks |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `rspec` | the Ruby suite, on Ruby 3.2/3.3 × Rails 7.0/7.1/7.2/8.0 — eight cells |
|
|
66
|
+
| `rubocop` | style, plus the three custom cops below |
|
|
67
|
+
| `yard` | every public method carries YARD documentation |
|
|
68
|
+
| `js` | the Vite plugin's vitest suite, including its Ruby↔JS byte-parity cases, and a type-level test over the generated accessors |
|
|
69
|
+
| `benchmark` | render-pipeline allocations against a recorded baseline |
|
|
70
|
+
| `name-propagation` | greps the tree for names retired before v0.1.0 |
|
|
71
|
+
| `gate` | that `CHANGELOG.md` gained something if this branch changed what the published gem contains, and that a version bump is one legal semver step to a version nothing has released |
|
|
72
|
+
|
|
73
|
+
<!-- ci-jobs:end -->
|
|
74
|
+
|
|
75
|
+
There is one more job, `release`, which publishes to RubyGems. It never runs on a pull request, and on a
|
|
76
|
+
push to `main` it publishes only when `lib/ruact/version.rb` names a version that carries no tag — which is
|
|
77
|
+
a thing a maintainer does deliberately, in a pull request of its own. See [RELEASING.md](RELEASING.md) for
|
|
78
|
+
the release process itself — this document deliberately describes none of it, so the two cannot drift apart.
|
|
79
|
+
|
|
80
|
+
Locally:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bundle exec rspec
|
|
84
|
+
bundle exec rubocop --format github
|
|
85
|
+
rm -rf .yardoc doc && bundle exec yard --fail-on-warning
|
|
86
|
+
bundle exec rake benchmark:memory
|
|
87
|
+
bin/release-gate origin/main HEAD
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
and, for the `js` job:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
cd vendor/javascript/vite-plugin-ruact
|
|
94
|
+
npm test
|
|
95
|
+
npm run typecheck
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
To reproduce one matrix cell other than the default — `RAILS_VERSION` is what the eight `rspec` cells vary,
|
|
99
|
+
and unset means 8.0:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
RAILS_VERSION=7.2 bundle install && bundle exec rspec
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Trying your working tree in a real app
|
|
106
|
+
|
|
107
|
+
The suite is fast and the gates are strict, and neither tells you whether your change works in a Rails
|
|
108
|
+
app. This does, in about two minutes. `RUACT` is the absolute path to your clone.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
export RUACT=/absolute/path/to/your/ruact
|
|
112
|
+
|
|
113
|
+
rails new myapp --skip-javascript
|
|
114
|
+
cd myapp
|
|
115
|
+
printf '\ngem "ruact", path: "%s"\n' "$RUACT" >> Gemfile
|
|
116
|
+
bundle install
|
|
117
|
+
bin/rails generate ruact:install
|
|
118
|
+
bin/dev
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
No published gem and no published npm package take part: the `path:` source points Bundler at your
|
|
122
|
+
checkout, and the generated `vite.config.js` imports the Vite plugin from inside it. Both halves of your
|
|
123
|
+
change are live.
|
|
124
|
+
|
|
125
|
+
Give it something to render — a component in `app/javascript/components/`:
|
|
126
|
+
|
|
127
|
+
```jsx
|
|
128
|
+
"use client";
|
|
129
|
+
import { useState } from "react";
|
|
130
|
+
|
|
131
|
+
export function LikeButton({ likes }) {
|
|
132
|
+
const [count, setCount] = useState(likes);
|
|
133
|
+
return <button onClick={() => setCount(count + 1)}>{count} likes</button>;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
a controller action, and a view that uses the component by name:
|
|
138
|
+
|
|
139
|
+
```erb
|
|
140
|
+
<h1>Home</h1>
|
|
141
|
+
<LikeButton likes={@likes} />
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`bin/dev` runs Rails and Vite together. Load the page and the button counts up.
|
|
145
|
+
|
|
146
|
+
## Four gates that fail for reasons your diff does not show
|
|
147
|
+
|
|
148
|
+
Most of CI fails where you expect. These four do not, so they are worth knowing before they surprise you.
|
|
149
|
+
|
|
150
|
+
1. **`spec/readme_spec.rb`** — `README.md` is pinned **byte for byte** in places: the quick-start block and
|
|
151
|
+
the demo image reference are compared against literals in the spec, the file is allowed exactly one raw
|
|
152
|
+
`<img>`, and a `path:` gem source is forbidden there because a reader who copies one installs nothing.
|
|
153
|
+
Edit the README casually and this goes red.
|
|
154
|
+
|
|
155
|
+
⚠️ **One of its assertions is repository-wide despite living in a README spec.** It runs `git ls-files`
|
|
156
|
+
over the whole repository and fails if any tracked file is a `.gif`, `.mp4`, `.webm` or `.webp` — media
|
|
157
|
+
committed here would ship inside every built `.gem` and stay in the clone history forever. So **no
|
|
158
|
+
document in this repository can carry an image**, including this one. Media lives with the documentation
|
|
159
|
+
site and is referenced by absolute URL.
|
|
160
|
+
|
|
161
|
+
⚠️ **YARD reads `README.md`.** Curly braces in its prose — `{@likes}`, `{Foo#bar}` — are link macros to
|
|
162
|
+
YARD, and an unresolvable one turns the `yard` job red for a README edit. Write them as `{` and
|
|
163
|
+
`}` there.
|
|
164
|
+
|
|
165
|
+
2. **`spec/readme_demo_message_spec.rb`** — the second half of that pin, covering the demo's message.
|
|
166
|
+
|
|
167
|
+
3. **`spec/benchmarks/render_pipeline_benchmark_spec.rb`** — an **allocation** baseline at ×1.20 tolerance
|
|
168
|
+
against `spec/benchmarks/baseline.json`, shared by matrix cells that legitimately differ by around 9%.
|
|
169
|
+
When it fails, read the header comment at the top of that file before doing anything: it explains how to
|
|
170
|
+
tell a real regression from accumulated drift, and how to regenerate the baseline if it is drift.
|
|
171
|
+
|
|
172
|
+
4. **Three custom RuboCop cops**, in `lib/rubocop/cop/ruact/` and enabled in `.rubocop.yml` —
|
|
173
|
+
`Ruact/NoSharedState`, `Ruact/NoIoInFlight` and `Ruact/NoExtendSelf`. Between them they say that data
|
|
174
|
+
flows through explicit arguments rather than shared mutable state, that `lib/ruact/flight/**` stays a
|
|
175
|
+
pure value transformation, and that modules expose explicit class methods.
|
|
176
|
+
|
|
177
|
+
Custom cops are the least discoverable failure a newcomer can hit, which is why they are named here —
|
|
178
|
+
but **the cop source is the authority for what each one actually detects**, and each detects a subset of
|
|
179
|
+
the rule it is named for rather than the rule entire. `Ruact/NoExtendSelf` also rejects a bare
|
|
180
|
+
`module_function`, which its name does not suggest. Read the file before assuming a pattern is either
|
|
181
|
+
caught or allowed.
|
|
182
|
+
|
|
183
|
+
And one habit rather than a gate: run `yard` after `rm -rf .yardoc doc`, as the block above does. A warm
|
|
184
|
+
cache hides warnings that CI, which checks out fresh, still reports.
|
|
185
|
+
|
|
186
|
+
## Where a change goes
|
|
187
|
+
|
|
188
|
+
Every destination below is one you can reach.
|
|
189
|
+
|
|
190
|
+
| Change | Where |
|
|
191
|
+
|---|---|
|
|
192
|
+
| anything under `lib/`, `app/`, `exe/`, `sig/`, `spec/` | **here** — `luizcg/ruact` |
|
|
193
|
+
| a typo in `README.md`, `CHANGELOG.md` or this file | **here** |
|
|
194
|
+
| the Vite plugin | **here**, at `vendor/javascript/vite-plugin-ruact/` — see the note below |
|
|
195
|
+
| the browser runtime for server functions | **here**, at `vendor/javascript/ruact-server-functions-runtime/` |
|
|
196
|
+
| the documentation site at `ruact.dev` | not in this repository — [open an issue](https://github.com/luizcg/ruact/issues) |
|
|
197
|
+
| roadmap, architecture decisions, design | not in this repository — [open an issue](https://github.com/luizcg/ruact/issues) or a discussion |
|
|
198
|
+
|
|
199
|
+
**About the Vite plugin.** There is a `vite-plugin-ruact` package on npm. It is **superseded**: nothing in
|
|
200
|
+
ruact installs it, and the plugin that actually runs is the vendored one in this repository. Send plugin
|
|
201
|
+
changes here.
|
|
202
|
+
|
|
203
|
+
## Where the design work happens
|
|
204
|
+
|
|
205
|
+
Roadmap, architecture decisions and story planning live in a private repository, so the reasoning behind a
|
|
206
|
+
change is not always visible from here. That does not gate anything: open an issue or a discussion, and the
|
|
207
|
+
outcome of that thinking comes back to you in the thread — you do not need access to propose a change,
|
|
208
|
+
argue for one, or land one.
|
|
209
|
+
|
|
210
|
+
## Opening the pull request
|
|
211
|
+
|
|
212
|
+
Branch off `main`, keep one topic per pull request, and open it against `luizcg/ruact`. Before you do:
|
|
213
|
+
|
|
214
|
+
- [ ] `bundle exec rspec` passes.
|
|
215
|
+
- [ ] `bundle exec rubocop --format github` is clean.
|
|
216
|
+
- [ ] `rm -rf .yardoc doc && bundle exec yard --fail-on-warning` passes, and every new public method has a
|
|
217
|
+
YARD comment with `@param` and `@return`.
|
|
218
|
+
- [ ] `# frozen_string_literal: true` is the first line of every new Ruby file.
|
|
219
|
+
- [ ] Nothing you added lives in `Ruact::`'s way: gem code stays under `Ruact::`, wire-format code under
|
|
220
|
+
`Ruact::Flight::`, and no Ruby or Rails core class is reopened.
|
|
221
|
+
- [ ] No `extend self` and no bare `module_function` — explicit class methods or a class instead.
|
|
222
|
+
- [ ] If you touched the Vite plugin or the runtime, `npm test` and `npm run typecheck` pass in
|
|
223
|
+
`vendor/javascript/vite-plugin-ruact`.
|
|
224
|
+
- [ ] `bin/release-gate origin/main HEAD` passes. If your branch changes anything that goes inside the
|
|
225
|
+
published gem, it asks [CHANGELOG.md](CHANGELOG.md) for a new bullet under `[Unreleased]` — written
|
|
226
|
+
for a reader who did not see the diff.
|
|
227
|
+
|
|
228
|
+
Describe what the change does and why in the pull request body. If it fixes a reported bug, link the issue.
|
|
229
|
+
|
|
230
|
+
Security issues do **not** go in a pull request — see [SECURITY.md](SECURITY.md) for private reporting.
|
|
231
|
+
|
|
232
|
+
By contributing you agree that your contribution is licensed under the MIT license in
|
|
233
|
+
[LICENSE.txt](LICENSE.txt).
|
data/README.md
CHANGED
|
@@ -58,7 +58,9 @@ export function PostCard({ post, author }) {
|
|
|
58
58
|
}
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
Capitalized tag means React. Lowercase stays HTML. `"use client"` is the only directive you need to learn.
|
|
62
|
+
|
|
63
|
+
Your ERB view renders on the server the way it always did, and ruact sends the result to the browser as a React tree — in the same [wire format](https://ruact.dev/docs/concepts/flight-wire-format) React uses for Server Components, implemented in Ruby. The data the view needs is inlined in the page, so React renders immediately, without a fetch. Node builds the bundle with Vite and is needed nowhere else.
|
|
62
64
|
|
|
63
65
|
## Call Rails from React
|
|
64
66
|
|
|
@@ -86,12 +88,12 @@ The verb decides — there is no per-action DSL and no second endpoint. The expo
|
|
|
86
88
|
|
|
87
89
|
## What you get
|
|
88
90
|
|
|
89
|
-
Every item below is shipped in this gem at v0.0.
|
|
91
|
+
Every item below is shipped in this gem at v0.0.11:
|
|
90
92
|
|
|
91
93
|
- **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
94
|
- **`"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
|
|
94
|
-
- **
|
|
95
|
+
- **Server functions and queries** — `include Ruact::Server` and `Ruact::Query` + `useQuery`, both reachable through a module generated from your route table. A query's declared keywords become an exact TypeScript signature; an action's accessor is typed to accept an object or a `FormData` and resolve whatever the action answers. [Docs](https://ruact.dev/docs/api/server-actions)
|
|
96
|
+
- **An opt-in allowlist for props** — by default a model prop is serialized with `as_json`, so every attribute crosses and the log says so. `include Ruact::Serializable` + `ruact_props :id, :title` makes it an allowlist, and `strict_serialization` (on in production) turns the permissive path into an error rather than a warning. [Docs](https://ruact.dev/docs/api/serializable)
|
|
95
97
|
- **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
98
|
- **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
99
|
- **Client-side navigation** — link interception, scroll restoration and redirect-after-POST, derived from your Rails routes. [Docs](https://ruact.dev/docs/concepts/navigation)
|
|
@@ -112,6 +114,12 @@ That is the whole adaptation, and it holds for any tool that outputs standard Re
|
|
|
112
114
|
|
|
113
115
|
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).
|
|
114
116
|
|
|
117
|
+
## Where ruact fits
|
|
118
|
+
|
|
119
|
+
**A good fit:** a Rails monolith that needs real React on real screens — dashboards, editors, admin tools, anything with interaction that ERB alone makes painful — with one team shipping both sides and no appetite for a second application to keep in sync.
|
|
120
|
+
|
|
121
|
+
**Not a fit yet:** pages that must render without JavaScript, or content search engines need to read out of the HTML. ruact renders client-side from a payload inlined in the page; there is no server-rendered HTML for the React tree. That is a real gap, not a roadmap wink — if the page is your SEO surface, keep it in ERB (the two coexist in the same app, per-view) or use something that server-renders.
|
|
122
|
+
|
|
115
123
|
## Compatibility
|
|
116
124
|
|
|
117
125
|
| | Version | Where that comes from |
|
|
@@ -136,6 +144,8 @@ Everything lives at [ruact.dev](https://ruact.dev):
|
|
|
136
144
|
|
|
137
145
|
Bug reports and pull requests are welcome at [github.com/luizcg/ruact/issues](https://github.com/luizcg/ruact/issues).
|
|
138
146
|
|
|
147
|
+
Setup, the checks a PR runs, and how to try your working tree in a real Rails app: [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
148
|
+
|
|
139
149
|
Release process: [RELEASING.md](RELEASING.md). Security policy and private reporting: [SECURITY.md](SECURITY.md).
|
|
140
150
|
|
|
141
151
|
## License
|
data/RELEASING.md
CHANGED
|
@@ -1,209 +1,195 @@
|
|
|
1
1
|
# Releasing ruact
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A release publishes exactly one artifact: the `ruact` gem, to RubyGems.
|
|
4
4
|
|
|
5
|
-
The
|
|
5
|
+
The Vite plugin is not a second artifact. It ships **inside** the gem, under `vendor/javascript/`, together with
|
|
6
|
+
the browser runtime; a generated app imports the plugin by filesystem path off the installed gem. Neither
|
|
7
|
+
bundled package is published, so neither is co-versioned with anything — whatever version number their
|
|
8
|
+
`package.json` files carry is internal to the gem and is not a release. The standalone `vite-plugin-ruact`
|
|
9
|
+
package on npm is a superseded artifact from before the plugin was vendored; no ruact release touches it, and
|
|
10
|
+
nothing this document describes puts anything there.
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
**The pull request is the release.** You write the version into `lib/ruact/version.rb` and stamp the CHANGELOG
|
|
13
|
+
in the same pull request; merging it is what publishes. There is no switch to turn on beforehand and none to
|
|
14
|
+
remember afterwards, and a merge that carries no new version publishes nothing — its content accumulates under
|
|
15
|
+
`[Unreleased]` until somebody decides to cut a release.
|
|
16
|
+
|
|
17
|
+
That is the whole of this document, and it is why almost every hand step this file used to prescribe has been
|
|
18
|
+
deleted rather than corrected: the workflow builds, uploads and tags, and it writes nothing you would otherwise
|
|
19
|
+
write yourself.
|
|
8
20
|
|
|
9
|
-
|
|
21
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the checks that run on every push and pull request and how to run
|
|
22
|
+
them locally — this document deliberately describes none of them, so the two cannot drift apart.
|
|
23
|
+
|
|
24
|
+
---
|
|
10
25
|
|
|
11
|
-
|
|
26
|
+
## Who owns what
|
|
12
27
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
| | Owner |
|
|
29
|
+
|---|---|
|
|
30
|
+
| Whether this merge publishes | you — by putting a new version in the pull request, or not |
|
|
31
|
+
| The version number | you — typed, in `lib/ruact/version.rb` |
|
|
32
|
+
| What the CHANGELOG entry says | you, in the same pull request |
|
|
33
|
+
| The build, the upload to RubyGems, the tag | the `release` job, on the push to `main` |
|
|
34
|
+
| Anything committed to `main` | nobody but a merge — the job pushes a tag and nothing else |
|
|
19
35
|
|
|
20
36
|
---
|
|
21
37
|
|
|
22
|
-
##
|
|
38
|
+
## 1. Decide the version
|
|
23
39
|
|
|
24
|
-
|
|
40
|
+
SemVer, against the public API: a breaking change to what consumers call (a renamed public method, a changed
|
|
41
|
+
signature, a removed configuration option, an incompatible change to the Flight wire format) is a major; a
|
|
42
|
+
backwards-compatible feature is a minor; everything else is a patch.
|
|
25
43
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
| Bug fix, patch | Patch: `X.Y.Z+1` | `0.1.0` → `0.1.1` |
|
|
44
|
+
Nothing computes this for you and nothing defaults. That is deliberate: the number used to be derived from a
|
|
45
|
+
default plus a marker in a commit message, which meant a minor could arrive as a side effect of how a merge was
|
|
46
|
+
worded — and the first one would have landed on the version this project has reserved for a milestone it has
|
|
47
|
+
not reached.
|
|
31
48
|
|
|
32
|
-
|
|
49
|
+
A minor or a major also moves the supported-versions table in [SECURITY.md](SECURITY.md). Do it in the same
|
|
50
|
+
pull request; nothing checks it.
|
|
33
51
|
|
|
34
52
|
---
|
|
35
53
|
|
|
36
|
-
##
|
|
37
|
-
|
|
38
|
-
### 1. Create a release branch
|
|
54
|
+
## 2. Prepare the release
|
|
39
55
|
|
|
40
56
|
```bash
|
|
41
|
-
|
|
57
|
+
bin/release X.Y.Z
|
|
42
58
|
```
|
|
43
59
|
|
|
44
|
-
|
|
60
|
+
It refuses a dirty checkout, and one that is not `main` exactly as the remote has it — a branch is never
|
|
61
|
+
*behind* `main` either, and cutting a release from one would put whatever it carries into the release. It
|
|
62
|
+
refuses a version that is not one step from the current one, and one that is already tagged. Then it writes `lib/ruact/version.rb`, re-resolves `Gemfile.lock` against
|
|
63
|
+
it, moves the accumulated `[Unreleased]` content in [CHANGELOG.md](CHANGELOG.md) under a dated heading, opens a
|
|
64
|
+
fresh empty `[Unreleased]`, rewrites both link references, proves the result against the changelog checks and
|
|
65
|
+
the release gate, commits, pushes, and opens the pull request.
|
|
45
66
|
|
|
46
|
-
|
|
67
|
+
**The pull-request body it writes is the checklist**, with the version already substituted. There is no
|
|
68
|
+
checklist document in this repository on purpose: a checklist is a second copy of the process in the
|
|
69
|
+
imperative, and it rots the way every other second copy does. This one is generated by the thing that performs
|
|
70
|
+
the process, on every release.
|
|
47
71
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
VERSION = "{X.Y.Z}"
|
|
51
|
-
end
|
|
52
|
-
```
|
|
72
|
+
You can do all of it by hand — the script performs no step you could not — but then you own the parts it
|
|
73
|
+
proves, and the gate on the pull request is where you find out.
|
|
53
74
|
|
|
54
|
-
|
|
75
|
+
CHANGELOG.md deserves one warning of its own, because it has three readers who share no directory: this
|
|
76
|
+
repository, a `.gem` unpacked on somebody's disk, and the generated changelog page on the documentation site.
|
|
77
|
+
An entry may name a file but must not **link** one relatively, because a target that resolves here 404s in the
|
|
78
|
+
other two. Absolute URLs only, or no link at all.
|
|
55
79
|
|
|
56
|
-
|
|
80
|
+
---
|
|
57
81
|
|
|
58
|
-
|
|
59
|
-
{
|
|
60
|
-
"version": "{X.Y.Z}"
|
|
61
|
-
}
|
|
62
|
-
```
|
|
82
|
+
## 3. Merge
|
|
63
83
|
|
|
64
|
-
|
|
84
|
+
On the resulting push to `main`, the `release` job — which waits on every other job in the workflow — asks one
|
|
85
|
+
question: **does `lib/ruact/version.rb` name a version that has no tag?**
|
|
65
86
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
3. Add or update the link footer at the bottom:
|
|
69
|
-
```
|
|
70
|
-
[Unreleased]: https://github.com/luizcg/ruact/compare/v{X.Y.Z}...HEAD
|
|
71
|
-
[{X.Y.Z}]: https://github.com/luizcg/ruact/compare/v{PREV}...v{X.Y.Z}
|
|
72
|
-
```
|
|
73
|
-
4. If this release contains breaking changes, ensure the section includes a `[BREAKING]` subsection with a **Migration Guide** (see "Breaking Changes" section below).
|
|
87
|
+
If it does not, the job reports that there is nothing to publish and ends green. That is the ordinary case;
|
|
88
|
+
most merges are not releases.
|
|
74
89
|
|
|
75
|
-
|
|
90
|
+
If it does, the job authenticates to RubyGems through Trusted Publishing (OIDC) — a short-lived credential
|
|
91
|
+
minted for that run, no stored secret, nothing interactive — then builds the gem, uploads it, and **tags last**.
|
|
76
92
|
|
|
77
|
-
|
|
93
|
+
**Why the tag comes last.** The tag is the record of a publish that finished. An upload that fails leaves no
|
|
94
|
+
tag, so the version is still untagged, so the next green push to `main` tries again — automatically, with
|
|
95
|
+
nothing to unwind and nobody to remember. The cost of that order is a re-run that meets a version already on
|
|
96
|
+
RubyGems, which is why a duplicate-version rejection is treated as *already done* rather than as a failure.
|
|
78
97
|
|
|
79
|
-
|
|
98
|
+
**Why a question about state rather than about this push.** A trigger that fired on "the version changed in
|
|
99
|
+
this push" would be lost forever whenever no run is created for a push — which happens, merge commits
|
|
100
|
+
included. The version would then say one thing, RubyGems another, and the next release would skip the number
|
|
101
|
+
entirely. Asking about state instead makes the job idempotent and self-healing: run it twice and the second
|
|
102
|
+
does nothing; miss it once and the next green push repairs it.
|
|
80
103
|
|
|
81
|
-
|
|
82
|
-
git add gem/lib/ruact/version.rb \
|
|
83
|
-
packages/vite-plugin-ruact/package.json \
|
|
84
|
-
gem/CHANGELOG.md \
|
|
85
|
-
packages/vite-plugin-ruact/CHANGELOG.md
|
|
86
|
-
git commit -m "Release v{X.Y.Z}"
|
|
87
|
-
```
|
|
104
|
+
---
|
|
88
105
|
|
|
89
|
-
|
|
106
|
+
## 4. Verify
|
|
90
107
|
|
|
91
108
|
```bash
|
|
92
|
-
|
|
109
|
+
curl -s https://rubygems.org/api/v1/versions/ruact/latest.json
|
|
93
110
|
```
|
|
94
111
|
|
|
95
|
-
|
|
112
|
+
Use that endpoint. The other one — `/gems/ruact.json` — is CDN-cached and keeps serving the previous version
|
|
113
|
+
for minutes after a successful publish, which reads exactly like a release that did not happen.
|
|
96
114
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Merge the PR. Confirm all CI jobs pass on the merge commit, then tag the verified merge commit:
|
|
115
|
+
---
|
|
100
116
|
|
|
101
|
-
|
|
102
|
-
git checkout main
|
|
103
|
-
git pull origin main
|
|
104
|
-
git tag v{X.Y.Z}
|
|
105
|
-
git push origin v{X.Y.Z}
|
|
106
|
-
```
|
|
117
|
+
## 5. When nothing was published and nothing said so
|
|
107
118
|
|
|
108
|
-
|
|
119
|
+
From the outside, a release that did not happen and one that did look identical until you check RubyGems. Every
|
|
120
|
+
case below shares one recovery, and it is the reason the trigger has the shape it has: **the version is still
|
|
121
|
+
untagged, so the next green push to `main` publishes it.** Nothing has to be unwound and nothing has to be
|
|
122
|
+
remembered.
|
|
109
123
|
|
|
110
|
-
|
|
124
|
+
**A job the `release` job waits on went red on the push to `main`.** The release job never ran, the merge has
|
|
125
|
+
landed, and no error anywhere says "no release happened". The run that matters is the one on `main`, not the
|
|
126
|
+
one on the pull request:
|
|
111
127
|
|
|
112
128
|
```bash
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
gem push ruact-{X.Y.Z}.gem
|
|
116
|
-
# Enter OTP when prompted (MFA is required)
|
|
117
|
-
rm ruact-{X.Y.Z}.gem
|
|
129
|
+
gh run list --branch main -L 3
|
|
130
|
+
gh run rerun <run-id> --failed
|
|
118
131
|
```
|
|
119
132
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
> gem contents ruact-{X.Y.Z} | grep -E 'CHANGELOG|RELEASING|SECURITY'
|
|
123
|
-
> ```
|
|
133
|
+
**No run was created for the push at all.** Same state, same repair; there is simply no run to re-run. Push
|
|
134
|
+
something green to `main` — the next merge does it on its own.
|
|
124
135
|
|
|
125
|
-
|
|
136
|
+
**The upload failed, or Trusted Publishing rejected the run.** Nothing was tagged, because the tag is written
|
|
137
|
+
after the upload. Fix the cause and let the next green push reconcile it.
|
|
126
138
|
|
|
127
|
-
|
|
128
|
-
cd packages/vite-plugin-ruact
|
|
129
|
-
npm publish
|
|
130
|
-
```
|
|
139
|
+
**The version is on RubyGems but the API still shows the old one.** CDN cache; see §4.
|
|
131
140
|
|
|
132
|
-
|
|
141
|
+
**A release was prepared and should not go out.** If it has not merged, close the pull request — nothing was
|
|
142
|
+
released and nothing has to be undone. If it has, see §6.
|
|
133
143
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
4. Body: paste the `## [{X.Y.Z}]` section from `gem/CHANGELOG.md`
|
|
138
|
-
5. Publish
|
|
144
|
+
**The suite is red on `main` because the record and the code disagree.** The changelog checks run in a job the
|
|
145
|
+
release job waits on, so once `lib/ruact/version.rb` and `CHANGELOG.md` stop naming the same version, **every**
|
|
146
|
+
later release waits too. The failure names which of the two moved.
|
|
139
147
|
|
|
140
148
|
---
|
|
141
149
|
|
|
142
|
-
##
|
|
143
|
-
|
|
144
|
-
When a release contains a breaking change:
|
|
150
|
+
## 6. Stopping a release that has already merged
|
|
145
151
|
|
|
146
|
-
|
|
147
|
-
2. Mark the CHANGELOG entry with `[BREAKING]` and include a **Migration Guide** sub-section:
|
|
152
|
+
Publication is irreversible, so there is a stop — a repository variable, read by the job itself:
|
|
148
153
|
|
|
149
|
-
|
|
150
|
-
|
|
154
|
+
```bash
|
|
155
|
+
gh variable set RUACT_RELEASE_HALT -b stop -R luizcg/ruact
|
|
156
|
+
```
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
- [BREAKING] `Ruact::Serializable.ruact_props` now rejects non-Symbol arguments at class-load time
|
|
158
|
+
Any non-empty value stops it. Unset — the normal, permanent state — publishes.
|
|
154
159
|
|
|
155
|
-
|
|
160
|
+
**A halted run fails red, naming the version it refused.** That is the point of it: the stop is a state
|
|
161
|
+
somebody has to undo, and a stop that produced a quiet, successful-looking run would suppress every later
|
|
162
|
+
release invisibly. This one reddens `main` on the next merge instead, which is a thing you find out about.
|
|
156
163
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
include Ruact::Serializable
|
|
161
|
-
ruact_props "id", "title" # strings silently coerced
|
|
162
|
-
end
|
|
163
|
-
```
|
|
164
|
-
**After:**
|
|
165
|
-
```ruby
|
|
166
|
-
class Post
|
|
167
|
-
include Ruact::Serializable
|
|
168
|
-
ruact_props :id, :title # must be Symbols
|
|
169
|
-
end
|
|
170
|
-
```
|
|
171
|
-
```
|
|
164
|
+
```bash
|
|
165
|
+
gh variable delete RUACT_RELEASE_HALT -R luizcg/ruact
|
|
166
|
+
```
|
|
172
167
|
|
|
173
|
-
|
|
168
|
+
The version is still untagged while the halt is in place, so clearing it does not require another bump: the
|
|
169
|
+
next green push publishes what was held.
|
|
174
170
|
|
|
175
171
|
---
|
|
176
172
|
|
|
177
|
-
## Rollback
|
|
173
|
+
## 7. Rollback
|
|
178
174
|
|
|
179
|
-
If a
|
|
175
|
+
If a published version has a critical defect:
|
|
180
176
|
|
|
181
|
-
**RubyGems** (within 30 days):
|
|
182
177
|
```bash
|
|
183
|
-
gem yank ruact -v
|
|
178
|
+
gem yank ruact -v X.Y.Z
|
|
184
179
|
```
|
|
185
180
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
181
|
+
This is the one RubyGems command still run by a person rather than by the workflow, and it is the one place the
|
|
182
|
+
"nothing interactive" of §3 does not apply: the gem declares `rubygems_mfa_required`, so yanking needs your own
|
|
183
|
+
RubyGems credential and a one-time code. The workflow's OIDC credential is minted for its own run and is no
|
|
184
|
+
help here.
|
|
190
185
|
|
|
191
|
-
|
|
186
|
+
Then cut a patch release with the fix immediately. A yanked version with no replacement leaves anyone who
|
|
187
|
+
pinned it with nothing to move to. There is nothing to unpublish anywhere else — this process publishes to
|
|
188
|
+
RubyGems only.
|
|
192
189
|
|
|
193
190
|
---
|
|
194
191
|
|
|
195
|
-
##
|
|
196
|
-
|
|
197
|
-
**`gem push` fails with "MFA required"**: Run `gem signin` first and ensure your OTP authenticator is set up at https://rubygems.org/settings/edit.
|
|
192
|
+
## Where this ends
|
|
198
193
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
**CI fails on release branch**: Fix the issue on the release branch and push the fix. Wait for CI to pass before tagging. If you already pushed a tag pointing to a broken commit, delete it and recreate after the fix:
|
|
202
|
-
```bash
|
|
203
|
-
git tag -d v{X.Y.Z} # delete local tag
|
|
204
|
-
git push origin :refs/tags/v{X.Y.Z} # delete remote tag
|
|
205
|
-
# fix the issue, merge, then re-tag from the correct commit
|
|
206
|
-
git checkout main && git pull origin main
|
|
207
|
-
git tag v{X.Y.Z} && git push origin v{X.Y.Z}
|
|
208
|
-
```
|
|
209
|
-
Do not use `git push --force` on tags — force-pushing a tag rewrite history for anyone who has already fetched it.
|
|
194
|
+
The gem is published and verified. The rest of a release — the private planning side — is outside this
|
|
195
|
+
repository and outside this document.
|