Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
49fc1af
Derive Pro RSC client refs from the RSC graph
ihabadham Jun 2, 2026
758b0f2
Address review feedback on Pro RSC client refs
justin808 Jun 3, 2026
027bcce
Pin the Pro dummy RSC manifest resolver to the generator contract
justin808 Jun 3, 2026
a8b529d
Apply review-toolkit findings to the RSC client-ref changes
justin808 Jun 3, 2026
9778c50
Harden Pro RSC client-ref cleanup, staleness, and error handling
justin808 Jun 4, 2026
2fbac98
Apply optional PR review polish to Pro RSC client-ref changes
justin808 Jun 4, 2026
b016bef
Drop redundant clean from Pro dummy build:dev:watch
justin808 Jun 4, 2026
b10cafd
Harden RSC manifest staleness check and tidy review nits
justin808 Jun 4, 2026
bad9385
Collapse second RSC component scan and align registration-entry path …
justin808 Jun 4, 2026
6c06341
Hoist with_env original outside the ensure region
justin808 Jun 4, 2026
1f320a3
Prepare RSC manifest discovery for released RSC package
justin808 Jun 4, 2026
5390da4
Merge remote-tracking branch 'origin/ihabadham/feature/pro-generate-r…
justin808 Jun 4, 2026
2c0cbd9
Merge remote-tracking branch 'origin/main' into ihabadham/feature/pro…
justin808 Jun 4, 2026
204cb87
Avoid deleting production webpack output in RSC builds
justin808 Jun 4, 2026
5210130
Regenerate server artifacts for stale RSC inputs
justin808 Jun 4, 2026
3294b9a
Apply PR review refinements to RSC manifest client refs
justin808 Jun 4, 2026
d291365
Use released RSC package for manifest fixes
justin808 Jun 4, 2026
b5253a3
Tighten RSC generator spec assertion
justin808 Jun 4, 2026
4086372
Address RSC manifest review follow-ups
justin808 Jun 4, 2026
adf4480
Fix Pro RSC CSS href resolution
justin808 Jun 5, 2026
051023e
Exclude Developerway URL from CI link check
justin808 Jun 5, 2026
6ec51d8
Tolerate CI RSC stylesheet hint shape
justin808 Jun 5, 2026
ca4df9a
Apply review-suggestion polish to RSC manifest refs
justin808 Jun 5, 2026
69bd443
Preserve legacy RSC refs fallback
justin808 Jun 5, 2026
bcc60a6
Merge origin/main into RSC manifest refs
justin808 Jun 5, 2026
b1a6c9a
Bypass stale RSC refs during discovery builds
justin808 Jun 5, 2026
64124cb
Isolate RSC discovery build env
justin808 Jun 5, 2026
9d414ea
fix: harden RSC manifest discovery migration
justin808 Jun 5, 2026
267d8fc
Merge origin/main into PR 3556
justin808 Jun 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/commands/verify.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ Use this order unless the changed files make a narrower or broader set clearly a
- TypeScript package changes: run `pnpm run build`, package tests, `pnpm run lint`, and `pnpm run type-check`.
- Generated examples or scripts: run the relevant generator/script command plus formatting and linting.
- Documentation-only changes: run `pnpm start format.listDifferent`, sidebar validation for `docs/oss/` or `docs/pro/`, and `bin/check-links` for new or changed URLs. If committing, still run `bundle exec rubocop`; see Instructions step 3 for why this applies even to docs-only commits. RuboCop does not validate Markdown.
- `react_on_rails_pro/**/*.{js,ts,tsx,jsx,json,css,md}` changes: confirm the Pro package edit was approved per the `AGENTS.md` ask-first rule, then run `cd react_on_rails_pro && pnpm start format.listDifferent` (the Pro package's local Prettier check via its `nps` script).
- `react_on_rails_pro/**/*.rb` changes: confirm the Pro package edit was approved per the `AGENTS.md` ask-first rule, then run `bundle exec rubocop react_on_rails_pro/` and any targeted RSpec.
- `react_on_rails_pro/**/*.{js,ts,tsx,jsx,json,css,md}` changes: run `cd react_on_rails_pro && pnpm start format.listDifferent` (the Pro package's local Prettier check via its `nps` script) plus any focused tests for the changed surface.
- `react_on_rails_pro/**/*.rb` changes: run `(cd react_on_rails_pro && bundle exec rubocop --ignore-parent-exclusion)` and any targeted RSpec.
- GitHub Actions workflow changes: confirm the edit was approved per the `AGENTS.md` ask-first rule, then run `actionlint` and `yamllint .github/`. Do not run RuboCop on `.yml` files.
- Anything not listed above (for example, Rakefile edits, generator templates, RBS-only changes, or build scripts): apply the narrowest set of checks that covers the changed surface and explain the choice in the output.

Expand Down
23 changes: 10 additions & 13 deletions .claude/docs/3211-rsc-css-fouc-design.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Design: Fix CSS FOUC behind `'use client'` boundaries (issue #3211)

Status: approved 2026-06-03. Single PR (patch + renderer + tests).
Status: approved 2026-06-03. Single PR (released package + renderer + tests).

> **Update 2026-06-04:** The upstream manifest fix landed in
> `react-on-rails-rsc@19.0.5-rc.6`, which records `.css` siblings (plus `.mjs`
Expand All @@ -19,8 +19,8 @@ seconds after first paint — producing a visible flash of unstyled content

Two independent gaps cause this, and **both** must be closed:

1. **Manifest gap.** The patched `react-server-dom-webpack-plugin.js` shipped by
`react-on-rails-rsc@19.0.4` records only `.js` files for each client
1. **Manifest gap.** Versions of `react-server-dom-webpack-plugin.js` shipped
before `react-on-rails-rsc@19.0.5-rc.6` record only `.js` files for each client
reference. The `mini-css-extract-plugin` CSS sibling of the same chunk group
is dropped, so nothing downstream even _knows_ the CSS exists.
2. **Renderer gap.** The Pro RSC renderer never emits any CSS metadata — no
Expand All @@ -36,10 +36,9 @@ manifest spec that traps gap #1.

## Why the fix is entirely in this repo

The upstream package fix (`shakacode/react_on_rails_rsc#35`) is still open and
unpublished. There is no fixed release to depend on. So the manifest half ships
here as a `pnpm patch` against `19.0.4`, mirroring what rsc#35 will eventually
publish; when a fixed release lands we drop the patch.
The upstream package fix now ships in `react-on-rails-rsc@19.0.5-rc.6`, so this
repo consumes the released package directly instead of carrying a local
`pnpm patch`.

## The mechanism we rely on: React 19 `<link precedence>`

Expand All @@ -58,10 +57,9 @@ identically — so SSR and CSR are both covered with no CSR-specific code.

## Design

### Part A — Manifest plugin patch (record CSS)
### Part A — Manifest package fix (record CSS)

`patches/react-on-rails-rsc@19.0.4.patch`, applied via pnpm
`patchedDependencies`, rewrites the chunk-collection loop in
`react-on-rails-rsc@19.0.5-rc.6` rewrites the chunk-collection loop in
`dist/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js`.

Current (buggy) inner loop records at most the first `.js` per chunk and `break`s
Expand Down Expand Up @@ -182,14 +180,13 @@ navigation they decode and React hoists/dedupes/suspends commit identically.

| File | Change |
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `package.json` (`pnpm.patchedDependencies`) | register the patch |
| `patches/react-on-rails-rsc@19.0.4.patch` _(new)_ | collect `.css` into `css[]` |
| `package.json` / workspace package manifests | consume `react-on-rails-rsc@19.0.5-rc.6` with the upstream CSS manifest fix |
| `packages/react-on-rails-pro/src/resolveCssHrefs.ts` _(new)_ | manifest → ordered deduped hrefs |
| `cache/` client-manifest accessor | cached read of client manifest for the renderer, invalidated by file signature |
| `packages/react-on-rails-pro/src/capabilities/proRSC.ts` | wrap tree with `<link precedence>` before `renderToPipeableStream` |
| `…/spec/dummy/spec/requests/rsc_use_client_css_manifest_spec.rb` | unpend; assert `css` |
| `…/spec/dummy/e2e-tests/` | assert `<link precedence>` in `<head>` + computed bg color |
| `react_on_rails_rsc` types (consumed) | `ImportManifestEntry.css?: string[]` (via patch + local type) |
| `react_on_rails_rsc` types (consumed) | `ImportManifestEntry.css?: string[]` |

## Migration / compatibility

Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/autoreview/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ This is the portable core. Hold it regardless of which engine runs.
`AGENTS.md` high-risk / `full-ci` / `benchmark` categories. Run it after the primary review is
clean, keep it to one extra pass, and verify its findings the same way.
- If you reject a finding as intentional/not worth fixing, add a brief inline code comment only when it documents a real invariant or ownership decision a future reviewer should know.
- **Do not push just to review.** Push only when the user asked for push/ship/PR. Follow `AGENTS.md` git boundaries (Pro package edits are ask-first; never force-push `main`/`master`).
- **Do not push just to review.** Push only when the user asked for push/ship/PR. Follow `AGENTS.md` git boundaries (never force-push `main`/`master`).

## Step 1 - Pick the target

Expand Down Expand Up @@ -98,7 +98,7 @@ Use `AGENTS.md` and `/verify` for the actual check set. Before a closeout review

```bash
(cd react_on_rails && bundle exec rubocop) # CI-equivalent OSS Ruby lint
# Also, only when Pro Ruby or RuboCop config changed (Pro edits are ask-first per AGENTS.md):
# Also, only when Pro Ruby or RuboCop config changed:
(cd react_on_rails_pro && bundle exec rubocop --ignore-parent-exclusion)
```

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/pro-test-package-and-gem.yml
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,9 @@ jobs:
JEST_JUNIT_OUTPUT_DIR: ./jest
JEST_JUNIT_ADD_FILE_ATTRIBUTE: "true"

- name: Run Pro dummy Jest unit tests
run: cd spec/dummy && pnpm run test:js
Comment thread
justin808 marked this conversation as resolved.

- name: Store test results
uses: actions/upload-artifact@v4
if: always()
Expand Down
1 change: 1 addition & 0 deletions .lychee.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ exclude = [
'^https://tanstack\.com/query/latest/docs/framework/react/guides/ssr$', # Connection failed from CI
'^https://lodash\.com', # Connection reset from CI
'^https://vite-ruby\.netlify\.app/?$', # Intermittent connection resets from CI
'^https://www\.developerway\.com/posts/react-server-components-performance$', # Returns 503 from CI

# ============================================================================
# PLANNED DEPLOYMENTS NOT YET LIVE
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ For small, focused PRs (roughly 5 files changed or fewer and one clear purpose):
- Ensure all files end with a newline
- Let Prettier and RuboCop handle formatting — never format manually
- When adding docs under `docs/oss/` or `docs/pro/`, also add the doc ID to `docs/sidebars.ts` and run `script/check-docs-sidebar` — CI will fail otherwise. To intentionally exclude a doc from the sidebar, add its ID to `docs/.sidebar-exclusions` with a reason comment.
- Pro package edits do not require special approval beyond the normal boundaries below; run the Pro-specific lint/tests that cover the changed files.

### Ask First

Expand Down
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ After a release, run `/update-changelog` in Claude Code to analyze commits, writ

### [Unreleased]

#### Added

- **[Pro]** **RSC manifest client reference discovery during precompile**: Generated RSC Webpack configs now run `RSCReferenceDiscoveryPlugin` through the Shakapacker precompile hook to emit `rsc-client-references.json`, use that manifest for RSC client-reference bundling, and warn when the selected manifest is stale. The generator now pins `react-on-rails-rsc` to `19.0.5-rc.6`, the prerelease containing the discovery plugin export and RSC manifest CSS fixes, and the Pro peer range explicitly accepts that prerelease. [PR 3556](https://github.com/shakacode/react_on_rails/pull/3556) by [ihabadham](https://github.com/ihabadham).

#### Changed

- **[Pro]** **Updated the RSC rollout pin to `react-on-rails-rsc@19.0.5-rc.6`**: Pro RSC install docs, generator defaults, package metadata, and example guidance now point at `19.0.5-rc.6` while keeping React on the supported `19.0.x` range and documenting the temporary exact prerelease pin. `19.0.5-rc.6` ships the client-manifest CSS collection fix (record `.css` siblings, `.mjs` chunk support, href normalization) upstream, so the temporary local pnpm patch (`patches/react-on-rails-rsc@19.0.5-rc.5.patch`) has been removed. [PR 3577](https://github.com/shakacode/react_on_rails/pull/3577) by [justin808](https://github.com/justin808).
Expand All @@ -36,9 +40,8 @@ After a release, run `/update-changelog` in Claude Code to analyze commits, writ

#### Fixed

- **[Pro]** **RSC CSS no longer flashes unstyled (FOUC) behind `'use client'` boundaries**: CSS imported by a `'use client'` boundary in a true React Server Component tree is now preloaded instead of loading only as a side effect of the JS chunk evaluating. The published `react-on-rails-rsc@19.0.5-rc.6` package now records each client reference's `.css` siblings in the RSC client manifest, and the Pro RSC renderer emits `<link rel="stylesheet" precedence="ror-rsc">` for them inside the RSC payload so React 19 hoists the stylesheets into `<head>` and blocks paint until they load — on both server render and client-side navigation. Fixes [Issue 3211](https://github.com/shakacode/react_on_rails/issues/3211). [PR 3587](https://github.com/shakacode/react_on_rails/pull/3587) by [justin808](https://github.com/justin808).
- **[Pro]** **`react-on-rails-rsc` prerelease (RC) versions no longer mark the dependency tree invalid**: The `react-on-rails-pro` peer dependency on the optional `react-on-rails-rsc` is now `*`, so installing any coordinated `react-on-rails-rsc` build — including prereleases such as `react-on-rails-rsc@19.0.5-rc.6` — no longer makes `npm ls react-on-rails-rsc` fail with `ELSPROBLEMS`. npm's strict semver only lets a prerelease satisfy a comparator that shares its exact `major.minor.patch` tuple, so no bounded range — including the `>= 19.0.2 < 20.0.0` range introduced in [PR 3580](https://github.com/shakacode/react_on_rails/pull/3580) — can admit prereleases across the React 19 line (e.g. `19.0.5-rc.6`, `19.2.x-rc.*`) without enumerating every patch tuple. `react-on-rails-rsc` stays an optional peer that Pro resolves only on the React Server Components path; the supported pairing is React on Rails RSC on the React 19 line (currently `>= 19.0.2`), and a mismatched build fails loudly at bundle time through Pro's `react-on-rails-rsc/*` imports rather than relying on the peer-range warning. Fixes [Issue 3609](https://github.com/shakacode/react_on_rails/issues/3609). [PR 3616](https://github.com/shakacode/react_on_rails/pull/3616) by [justin808](https://github.com/justin808).

- **[Pro]** **RSC CSS no longer flashes unstyled (FOUC) behind `'use client'` boundaries**: CSS imported by a `'use client'` boundary in a true React Server Component tree is now preloaded instead of loading only as a side effect of the JS chunk evaluating. The RSC client manifest now records each client reference's `.css` siblings (via a `pnpm` patch to `react-on-rails-rsc` until the upstream fix is published), and the Pro RSC renderer emits `<link rel="stylesheet" precedence="ror-rsc">` for them inside the RSC payload so React 19 hoists the stylesheets into `<head>` and blocks paint until they load — on both server render and client-side navigation. Fixes [Issue 3211](https://github.com/shakacode/react_on_rails/issues/3211). [PR 3587](https://github.com/shakacode/react_on_rails/pull/3587) by [justin808](https://github.com/justin808).
- **[Pro]** **Client teardown failures are no longer hidden at `console.info`**: when `ComponentRenderer.unmount()` catches an error from `unmountComponentAtNode` (the React 16/17 legacy unmount path), it now logs at `console.error` instead of `console.info`. A caught error there means the component tree did not unmount cleanly — a teardown failure — and most log collectors and default browser-console filters drop `info`, so the failure was effectively silent. Addresses item 2 of [Issue 3592](https://github.com/shakacode/react_on_rails/issues/3592). [PR 3610](https://github.com/shakacode/react_on_rails/pull/3610) by [justin808](https://github.com/justin808).

### [17.0.0.rc.1] - 2026-06-02
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,9 @@ Check for spinners, skeleton screens, loading indicators. Use `javascript_tool`
- Dropdowns (`select`, custom dropdowns)
- Textareas
- Checkboxes and radio buttons

**Try filling them during probing** — actually type values into inputs, select dates on calendars, increment number fields, check checkboxes. This confirms what works and what doesn't before you write test code.

**Fill before clicking action buttons.** When a form has both inputs and a submit/action button (like "Book Now", "Search", "Apply"), the right test sequence is: fill all inputs first → capture the filled state → then click the button. A test that clicks "Book Now" without filling in dates and guests misses the most interesting UI state (the populated form) and may also miss validation behavior.

**A7. Probe inside modals/expanded UI**: when clicking reveals new UI (modal, drawer, expanded panel), probe THAT UI for its own interactive elements — buttons, forms, links within the modal. Keep going as long as new testable UI appears. For each form inside a modal, record all fields so you can write a form-fill test in Step B.
Expand Down
2 changes: 2 additions & 0 deletions packages/react-on-rails-pro/src/resolveCssHrefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export type RscCssManifest = {
const joinPrefix = (prefix: string, file: string): string => {
if (!prefix) return file;
const base = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
// Manifest CSS paths are usually root-relative, but callers may pass already
// prefixed or absolute hrefs; avoid double-prefixing either shape.
if (
file === base ||
file.startsWith(`${base}/`) ||
Expand Down
32 changes: 32 additions & 0 deletions packages/react-on-rails-pro/tests/resolveCssHrefs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,38 @@ describe('resolveCssHrefs', () => {
).toEqual(['https://cdn.example.com/assets/css/a.css', 'https://cdn.example.com/assets/css/b.css']);
});

it('does not double-prefix root-relative css files that already include the webpack prefix', () => {
expect(
resolveCssHrefs({
moduleLoading: { prefix: '/webpack/test/' },
filePathToModuleMetadata: {
'file:///app/SimpleClientComponent.jsx': {
id: '1',
chunks: [],
css: ['/webpack/test/css/client5-6dd89694.css'],
name: '*',
},
},
}),
).toEqual(['/webpack/test/css/client5-6dd89694.css']);
});

it('still prefixes a root-relative css file that does not already include the webpack prefix', () => {
expect(
resolveCssHrefs({
moduleLoading: { prefix: '/webpack/test/' },
filePathToModuleMetadata: {
'file:///app/SimpleClientComponent.jsx': {
id: '1',
chunks: [],
css: ['/css/client5-6dd89694.css'],
name: '*',
},
},
}),
).toEqual(['/webpack/test/css/client5-6dd89694.css']);
});

it('treats a missing prefix as empty (bare hrefs)', () => {
expect(
resolveCssHrefs({
Expand Down
1 change: 1 addition & 0 deletions react_on_rails/.rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ AllCops:

Exclude:
- 'spec/dummy/bin/*'
- 'spec/react_on_rails/dummy-for-generators/**/*' # Generated fixture contains intentionally invalid Ruby
- 'spike/**/*' # Exploratory spike code outside lib/ — not part of the production surface

Naming/FileName:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,18 +145,9 @@ module JsDependencyManager
# which pins `react-on-rails-rsc`. The two track different versions during the RC window:
# react/react-dom stay on stable 19.0.x while react-on-rails-rsc rides an RC.
RSC_REACT_VERSION_RANGE = "~19.0.4"
Comment thread
justin808 marked this conversation as resolved.
Comment thread
justin808 marked this conversation as resolved.
Comment thread
justin808 marked this conversation as resolved.
# Pinned to 19.0.5-rc.x because the native rspack manifest plugin
# (`react-on-rails-rsc/RspackPlugin`) the generator scaffolds for rspack projects is only
# exported from 19.0.5 onward. This single pin applies to ALL `--rsc` installs, so webpack
# projects are also pinned to this RC during the pre-stable window (intentional: the RC is
# backward-compatible for webpack — it still exports `react-on-rails-rsc/WebpackPlugin`). The
# #3488 stable bump below moves every bundler to 19.0.5 at once.
# TODO(#3488): when react-on-rails-rsc 19.0.5 stable ships, bump RSC_PACKAGE_VERSION_PIN to
# "19.0.5" (and RSC_REACT_VERSION_RANGE too if react/react-dom advance), then verify peer-dep
# alignment between react@19.0.x and react-on-rails-rsc@19.0.5. At that point the `latest`
# tag exports RspackPlugin, so the rspack-only fallback skip in add_rsc_dependencies can be
# removed (rspack can safely share the webpack unversioned-fallback path again).
# (tracked in https://github.com/shakacode/react_on_rails/issues/3488).
# Pinned to 19.0.5-rc.6 because the discovery plugin export, native Rspack plugin, and
# RSC manifest CSS fixes all ship in that prerelease.
# TODO(#3642): switch to a stable react-on-rails-rsc release after 19.0.5 stable ships.
RSC_PACKAGE_VERSION_PIN = "19.0.5-rc.6"
Comment thread
justin808 marked this conversation as resolved.

private
Expand Down Expand Up @@ -250,7 +241,7 @@ def add_react_dependencies
say "Installing React dependencies..."

# RSC requires React 19.0.x specifically (not 19.1.x or later)
# Pin to ~19.0.4 to allow patch updates while staying within 19.0.x
# Pin React to ~19.0.4 while using an RSC package release that exports manifest discovery.
Comment thread
justin808 marked this conversation as resolved.
react_deps = if respond_to?(:use_rsc?) && use_rsc?
["react@#{RSC_REACT_VERSION_RANGE}", "react-dom@#{RSC_REACT_VERSION_RANGE}",
"prop-types@^15.0.0"]
Expand Down
Loading
Loading