Skip to content

Derive Pro RSC client refs from the RSC graph#3556

Merged
justin808 merged 29 commits into
mainfrom
ihabadham/feature/pro-generate-rsc-manifest-refs
Jun 5, 2026
Merged

Derive Pro RSC client refs from the RSC graph#3556
justin808 merged 29 commits into
mainfrom
ihabadham/feature/pro-generate-rsc-manifest-refs

Conversation

@ihabadham

@ihabadham ihabadham commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • generate an RSC-only server component registration entry from the pack generator
  • run an RSC reference discovery build from the precompile hook
  • have Pro client/server manifest configs consume the discovered refs JSON instead of broad filesystem scanning
  • update the Pro dummy build scripts to run the precompile hook before bundling
  • consume released react-on-rails-rsc@19.0.5-rc.6 from npm
  • remove the old fork/patch path; rc.6 already includes the CSS manifest metadata fix needed here
  • upgrade existing broad rscClientReferences helpers to the manifest-backed helper during migration
  • keep discovery builds isolated from stale manifests and bundle-only env leakage

Supports #3553.

RSC package dependency: stay on react-on-rails-rsc@19.0.5-rc.6. I found no current gap that requires an rc.7. This PR should land after #3590 and preferably after #3616 so the generator and optional peer-range story are already in main.

Architecture decisions

  • The generated helper intentionally uses the discovery manifest as the source of truth, then falls back to broad scanning only when the app has not generated a manifest yet or the installed RSC setup does not expose discovery support.
  • Existing broad object-literal rscClientReferences helpers are not treated as fully migrated. The generator upgrades them to the manifest-backed helper so upgraded apps get exact RSC graph references instead of silently keeping broad scanning.
  • The generated fileContainsAll checks read each inspected file once per check. This avoids repeated disk reads without memoizing across long-running webpack config evaluation, where setup files may change while a developer is iterating.
  • build:dev:watch deletes stale development webpack output and ssr-generated before running the precompile hook. It deliberately does not delete production output.
  • The shared and standalone precompile-hook paths now preserve failure backtrace context consistently.

Verification

Review-fix commit 9d414eabe530902c8b5073a92b69f61dc4f40501:

  • pnpm install --frozen-lockfile
  • bundle exec rubocop react_on_rails/lib/generators/react_on_rails/rsc_setup/client_references.rb react_on_rails/spec/react_on_rails/generators/rsc_generator_spec.rb react_on_rails/spec/react_on_rails/shakapacker_precompile_hook_shared_spec.rb react_on_rails/spec/support/shakapacker_precompile_hook_shared.rb
  • pnpm exec prettier --check react_on_rails_pro/spec/dummy/tests/rsc-manifest-client-references.test.js react_on_rails_pro/spec/dummy/tests/package-scripts.test.js react_on_rails_pro/spec/dummy/config/webpack/rscManifestClientReferences.js react_on_rails_pro/spec/dummy/package.json
  • bundle exec rspec react_on_rails/spec/react_on_rails/generators/rsc_generator_spec.rb
  • bundle exec rspec react_on_rails/spec/react_on_rails/shakapacker_precompile_hook_shared_spec.rb
  • pnpm --dir react_on_rails_pro/spec/dummy run test:js -- --runInBand tests/rsc-manifest-client-references.test.js tests/package-scripts.test.js
  • git diff --check

Earlier implementation verification retained from the original PR:

  • cd react_on_rails/spec/dummy && bundle exec rspec spec/packs_generator_spec.rb
  • bundle exec rspec spec/requests/rsc_use_client_css_manifest_spec.rb from react_on_rails_pro/spec/dummy
  • NODE_CONDITIONS=react-server pnpm --dir packages/react-on-rails-pro exec jest tests/manifestLoaderServerRSC.rsc.test.ts --runInBand
  • pnpm --dir packages/react-on-rails-pro exec jest tests/manifestLoader.test.ts tests/resolveCssHrefs.test.ts --runInBand
  • pnpm run lint
  • pnpm start format.listDifferent
  • bundle exec rubocop from react_on_rails/
  • bundle exec rubocop --ignore-parent-exclusion from react_on_rails_pro/
  • bundle exec rubocop benchmarks
  • pnpm list react-on-rails-rsc --depth 0 -r
  • pnpm run build:dev from react_on_rails_pro/spec/dummy
  • pnpm run build:test from react_on_rails_pro/spec/dummy
  • pnpm run build:client from react_on_rails_pro/spec/dummy
  • pnpm run build:server from react_on_rails_pro/spec/dummy

Summary by CodeRabbit

  • New Features

    • Integrates RSC client-reference discovery into the precompile/build flow to emit a client-reference manifest for bundling.
  • Bug Fixes

    • Resolves RSC CSS behavior behind "use client" boundaries by consuming the upstream prerelease.
  • Chores

    • Bumped RSC package to 19.0.5-rc.6 and removed the local patch override.
    • Expanded tests and CI to cover RSC discovery, manifest handling, and precompile hook behavior.

Follow-up

Review follow-ups

Non-blocking review nits are tracked in #3642, including unresolved threads PRRT_kwDOAnNnU86HSorq, PRRT_kwDOAnNnU86HSpqC, and PRRT_kwDOAnNnU86HSp95. The two version-specific discovery-plugin error-message threads were addressed in the latest push and may remain visible only as outdated unresolved threads until reviewer cleanup.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Upgrades react-on-rails-rsc to 19.0.5-rc.6, removes the local patch, generates a server-component registration-entry, runs a discovery precompile to emit rsc-client-references.json, and wires generators/webpack/precompile/CI/tests to consume and validate the manifest-driven client-reference flow.

Changes

RSC Manifest-Driven Client Reference Discovery

Layer / File(s) Summary
Version pins and dependency updates
package.json, packages/react-on-rails-pro/package.json, react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb, .claude/docs/3211-rsc-css-fouc-design.md, CHANGELOG.md, packages/react-on-rails-pro/src/resolveCssHrefs.ts
react-on-rails-rsc upgraded to 19.0.5-rc.6, removed pnpm patch override, updated docs/changelog and generator constant.
PacksGenerator: Server component registration entry generation and management
react_on_rails/lib/react_on_rails/packs_generator.rb, react_on_rails/spec/dummy/spec/packs_generator_spec.rb
Adds server-component-registration-entry.js generation, non-entrypoints directory helpers, expected-file/cleanup handling, and staleness checks with spec coverage.
Generator: Manifest-backed RSC client references with staleness handling
react_on_rails/lib/generators/react_on_rails/rsc_setup/client_references.rb, react_on_rails/spec/react_on_rails/generators/rsc_generator_spec.rb
Emits fallbackRscClientReferences and an IIFE-backed rscClientReferences that resolves/parses/validates ssr-generated/rsc-client-references.json (or override), warns on staleness, and falls back in discovery/bundle-only modes; detection logic refactored.
Precompile hook: RSC manifest discovery orchestration
react_on_rails/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook, react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb
Hook invokes generate_rsc_manifest_client_references_if_needed after pack generation with recursion guard, support gate, stale-manifest cleanup, shakapacker validation, and discovery-only env flags.
Webpack RSC config template: Discovery mode entry and output wiring
react_on_rails/lib/generators/react_on_rails/templates/rsc/base/config/webpack/rscWebpackConfig.js.tt
RSC webpack template supports discovery mode: validates registration entry, rewires entry to rsc-reference-discovery, sets output filename and plugin list conditionally.
Precompile hook shared helpers: Discovery entry finding and build execution
react_on_rails/spec/support/shakapacker_precompile_hook_shared.rb, react_on_rails/spec/react_on_rails/shakapacker_precompile_hook_shared_spec.rb
Implements discovery helpers to find valid registration-entry paths, prune excluded directories, clear stale manifests, run discovery build, and tests covering env handling, path validation, and failure scenarios.
Pro dummy: RSC manifest client references resolver module
react_on_rails_pro/spec/dummy/config/webpack/rscManifestClientReferences.js, react_on_rails_pro/spec/dummy/tests/rsc-manifest-client-references.test.js
New resolver loads override or default manifest, validates payload.refs array, warns on staleness, falls back to discovery-directory when flags set, and throws a precompile-hook hint when registration entry exists but manifest missing; covered by Jest tests.
Pro dummy: Webpack configs wired to manifest resolver
react_on_rails_pro/spec/dummy/config/webpack/clientWebpackConfig.js, react_on_rails_pro/spec/dummy/config/webpack/serverWebpackConfig.js, react_on_rails_pro/spec/dummy/config/webpack/rscWebpackConfig.js
Client and server webpack configs call rscManifestClientReferences() for RSCWebpackPlugin clientReferences; rsc webpack config adopts discovery-mode wiring.
Pro dummy: Build scripts and package configuration
react_on_rails_pro/spec/dummy/package.json, react_on_rails_pro/spec/dummy/tests/package-scripts.test.js
Bumped react-on-rails-rsc in dummy to 19.0.5-rc.6, refactored build scripts to run bin/shakapacker-precompile-hook before bin/shakapacker and updated cleanup/watch scripts; added Jest tests validating scripts.
Configuration and CI updates
react_on_rails/.rubocop.yml, .github/workflows/pro-test-package-and-gem.yml
RuboCop excludes generated dummy fixture; CI workflow runs Pro dummy Jest unit tests as an additional step.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested labels

codex-wip

Suggested reviewers

  • AbanoubGhadban

Poem

🐰 I found the manifest, shiny and new,

No patches, just upstream magic in view.
Precompile hops, discovery in tow,
Registration entries tell webpack what to know.
CSS and refs aligned — hop! — ready to show.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title "Derive Pro RSC client refs from the RSC graph" clearly and concisely summarizes the main change: the PR refactors how Pro derives React Server Component client references by consuming the RSC graph instead of broad filesystem scanning.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ihabadham/feature/pro-generate-rsc-manifest-refs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread package.json Outdated
Comment thread react_on_rails/lib/react_on_rails/packs_generator.rb
Comment thread react_on_rails/lib/react_on_rails/packs_generator.rb Outdated
Comment thread react_on_rails/lib/generators/react_on_rails/rsc_setup/client_references.rb Outdated
Comment thread react_on_rails_pro/spec/dummy/config/webpack/rscManifestClientReferences.js Outdated
Comment thread react_on_rails_pro/spec/dummy/package.json Outdated
@claude

claude Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overview

This PR introduces RSC client-reference discovery driven by the actual RSC module graph rather than broad filesystem scanning:

  1. PacksGenerator emits server-component-registration-entry.js importing only registered server components.
  2. The precompile hook runs a discovery webpack build (RSC_REFERENCE_DISCOVERY_BUILD=true) via RSCReferenceDiscoveryPlugin, writing ssr-generated/rsc-client-references.json.
  3. Client/server webpack configs read that JSON, falling back to broad-scan when absent.

The design is sound and cleanly solves the unrelated use-client file contamination problem.


Blocker

The PR is labelled do-not-merge appropriately. The react-on-rails-rsc pin in package.json must be updated to point to the upstream shakacode/react_on_rails_rsc repo (not the contributor personal fork) before this can land. See inline comment on package.json:72.


Issues (inline comments posted)

Blocker — package.json:72: react-on-rails-rsc pinned to a personal fork commit; supply-chain and stability risk.

Medium — packs_generator.rb:391: server_component_registration_entries called twice inside server_component_registration_entry_content; the full component-discovery pipeline runs 4x per call. Fix: capture into a local variable once.

Medium — packs_generator.rb:428: generated_nonentrypoints_directory_path calls FileUtils.mkdir_p as a side-effect, meaning every read-only path query (staleness check, cleanup enumeration) silently creates the directory.

Medium — client_references.rb:1196-1199: generated_rsc_client_references_defined? relies on three plain String#include? checks. This is the same commented-out-code false-positive the regex+js_top_level_position? approach was written to prevent.

Medium — rscManifestClientReferences.js:48: Dummy file duplicates the JSON-loading logic from the generated template with no shared source; the two can silently diverge.

Low — rscManifestClientReferences.js:9: __dirname-relative path to ssr-generated/ ties the file to its two-levels-deep position. The generated template uses resolve() (CWD-relative) which is more portable.

Low — dummy/package.json:107: build:dev:watch skips the ssr-generated cleanup that build:dev and build:test include, so watch mode can reuse stale discovery output.


What looks good

  • Three-state fallback (configured JSON, default JSON, broad scan) degrades gracefully and the error message is actionable.
  • The recursive-call guard (SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true on the nested system call) is correct.
  • Test coverage for the registration-entry generator and updated scoped_rsc_client_references_defined? is solid.
  • Extracting generated_nonentrypoints_directory_path removes duplication cleanly.
  • The discoveryBuild flag in rscWebpackConfig.js.tt keeps the two build modes clearly separated.

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
react-on-rails/client bundled (gzip) 61.25 KB (0%)
react-on-rails/client bundled (gzip) (time) 61.25 KB (0%)
react-on-rails/client bundled (brotli) 52.52 KB (0%)
react-on-rails/client bundled (brotli) (time) 52.52 KB (0%)
react-on-rails-pro/client bundled (gzip) 62.42 KB (0%)
react-on-rails-pro/client bundled (gzip) (time) 62.42 KB (0%)
react-on-rails-pro/client bundled (brotli) 53.55 KB (0%)
react-on-rails-pro/client bundled (brotli) (time) 53.55 KB (0%)
registerServerComponent/client bundled (gzip) 71.73 KB (0%)
registerServerComponent/client bundled (gzip) (time) 71.73 KB (0%)
registerServerComponent/client bundled (brotli) 61.74 KB (0%)
registerServerComponent/client bundled (brotli) (time) 61.74 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) 65.48 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) (time) 65.48 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) 56.15 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) (time) 56.15 KB (0%)

@justin808

justin808 commented Jun 3, 2026

Copy link
Copy Markdown
Member

RSC-side update: I opened maintainer-owned draft replacement shakacode/react_on_rails_rsc#47 for shakacode/react_on_rails_rsc#46 because direct push to the contributor fork branch was rejected. shakacode/react_on_rails_rsc#47 carries shakacode/react_on_rails_rsc#46 plus review fixes for same-line use client detection, missing bundler API diagnostics, and missing client chunk block warnings. Unit tests, CodeRabbit, Cursor, and Claude review are passing there. Once the RSC dependency story is settled, this PR should point at the upstream shakacode/react_on_rails_rsc#47 head or the released package instead of the personal fork pin.

@justin808

Copy link
Copy Markdown
Member

shakacode/react_on_rails_rsc#47 is merged.

justin808 added a commit that referenced this pull request Jun 3, 2026
Resolves the claude-review comments on PR #3556:

- Repoint react-on-rails-rsc to the upstream shakacode repo at the same
  commit instead of a contributor fork. The upstream PR-head ref already
  contains that commit and GitHub archives are deterministic per SHA, so
  the codeload tarball is byte-identical and the lockfile integrity hash
  is unchanged. Keeps the install path off a personal fork while the dep
  is still pinned pending react_on_rails_rsc#46.
- packs_generator: capture server_component_registration_entries once in
  server_component_registration_entry_content instead of invoking the
  component-discovery pipeline four times per call.
- packs_generator: move FileUtils.mkdir_p out of the
  generated_nonentrypoints_directory_path accessor into a dedicated
  ensure_nonentrypoints_directory!, called only before the actual file
  writes, so read-only callers (staleness checks, cleanup enumeration)
  no longer create the directory as a side effect.
- dummy rscManifestClientReferences: resolve the refs JSON relative to
  process.cwd() (the Rails root) like the generated template, instead of
  coupling to this file's location via __dirname.
- client_references generator: detect the generated manifest fallback
  structurally via the module-scope fallbackRscClientReferences literal
  rather than brittle content.include? substring guards on the env-var
  and JSON filename, which coupled detection to template internals and
  added no safety. Adds regression tests locking the structural behavior.
- dummy package.json: add the public/webpack + ssr-generated cleanup step
  to build:dev:watch so a stale rsc-client-references.json is not reused.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Code Review: PR 3556 - Derive Pro RSC client refs from the RSC graph. Full review text follows in replies.

@claude

claude Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Overview - This PR replaces broad filesystem scanning for RSC clientReferences with a precise two-phase build: (1) pack generation emits server-component-registration-entry.js for only registered server components, (2) a precompile-hook discovery webpack build walks the RSC import graph and emits rsc-client-references.json via RSCReferenceDiscoveryPlugin, (3) client/server webpack configs read that JSON. The fallback-to-directory-scan behaviour preserves backward compatibility. ### Issue: Performance regression in staleness check - server_component_registration_entries is called on every generated_files_present_and_up_to_date? check (every Rails request in development). The method is not memoized and its call chain triggers multiple Dir.glob calls per invocation via components_for_server_registration being called twice inside server_component_registration_entries (once directly and once through server_component_names_for_registration). That is 4+ filesystem scans per staleness check, and the method is also called in build_expected_files_set and create_server_component_registration_entry. Memoizing components_for_server_registration would fix this. ### Issue: registerServerComponent call formatting - entries.keys.join(',newline') produces registerServerComponent({ComponentA, newline ComponentB}); - opening brace on the first name, no trailing comma, closing brace on the last. Valid JS but visually jarring. Consider join(', ') or a proper multi-line form. ### Issue: Git-pinned pre-release dependency - react-on-rails-rsc is pinned to commit a7f3838 (v19.0.5-rc.3). Noted as temporary in the PR description, but flagging as a merge blocker. ### Design positives - Clean separation of FileUtils.mkdir_p from generated_server_bundle_file_path into ensure_nonentrypoints_directory! is a clear improvement. Detection strategy in generated_rsc_client_references_defined? using the structural fallbackRscClientReferences literal (not string-matching env-var names) is correct and well-documented. ### Minor - Duplicated JSON-reading logic between client_references.rb generated template and rscManifestClientReferences.js - a cross-reference comment would help. - rsc_manifest_registration_entry_path? reads like a path accessor but returns a boolean; consider valid_rsc_registration_entry_path?. - No test for the rsc_client_references_defined? short-circuit in generated_rsc_client_references_defined?.

Comment thread react_on_rails/lib/react_on_rails/packs_generator.rb
Comment thread react_on_rails/lib/react_on_rails/packs_generator.rb
Comment thread react_on_rails/spec/support/shakapacker_precompile_hook_shared.rb Outdated
justin808 added a commit that referenced this pull request Jun 3, 2026
Addresses the remaining review comment on PR #3556 (duplicated logic between the
Pro dummy's hand-written rscManifestClientReferences.js and the inline block the
RSC setup generator emits via rsc_client_references_js).

Rather than extract a shared package module — which would force the generated
end-user config into an opaque require(), add a public react-on-rails export, and
rewrite the migration detection to match a magic require path (re-introducing the
brittleness just removed from generated_rsc_client_references_defined?) — this keeps
both copies but makes drift fail CI:

- Document the dummy file as an intentional mirror of the generator's resolution
  contract (override env var, default manifest path, { refs: [...] } shape, fallback
  ordering, precompile-hook error), and align its broad-fallback include regex to the
  template's (js|mjs|cjs|ts|mts|cts|jsx|tsx).
- Add a dummy-side Jest contract test exercising every branch of the resolver
  (env-var override, default manifest, invalid payload, discovery/bundle fallback,
  missing-manifest hint, no-manifest fallback).
- Add a generator-side RSpec assertion that the emitted config carries the same
  contract tokens, so a change to either side breaks a test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread react_on_rails/spec/support/shakapacker_precompile_hook_shared.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/packs_generator.rb
@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

const rscWebpackConfig = resolve('config/webpack/rscWebpackConfig.js');

P2 Badge Check the rspack RSC config path before falling back

In rspack projects the generator writes rscWebpackConfig.js under config/rspack/ via destination_config_path, but the emitted resolver always probes config/webpack/rscWebpackConfig.js. When a current rspack RSC app has a server-component registration entry but no generated manifest (for example after cleaning ssr-generated and running bin/shakapacker directly), rscConfigSupportsDiscovery() incorrectly returns false and silently falls back to the broad client-reference scan instead of enforcing the discovery manifest. That leaves rspack apps on the old behavior even though their generated configs can produce graph-derived refs.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

2 similar comments
@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

const rscWebpackConfig = resolve('config/webpack/rscWebpackConfig.js');

P2 Badge Check the rspack RSC config path before falling back

In rspack projects the generator writes rscWebpackConfig.js under config/rspack/ via destination_config_path, but the emitted resolver always probes config/webpack/rscWebpackConfig.js. When a current rspack RSC app has a server-component registration entry but no generated manifest (for example after cleaning ssr-generated and running bin/shakapacker directly), rscConfigSupportsDiscovery() incorrectly returns false and silently falls back to the broad client-reference scan instead of enforcing the discovery manifest. That leaves rspack apps on the old behavior even though their generated configs can produce graph-derived refs.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

const rscWebpackConfig = resolve('config/webpack/rscWebpackConfig.js');

P2 Badge Check the rspack RSC config path before falling back

In rspack projects the generator writes rscWebpackConfig.js under config/rspack/ via destination_config_path, but the emitted resolver always probes config/webpack/rscWebpackConfig.js. When a current rspack RSC app has a server-component registration entry but no generated manifest (for example after cleaning ssr-generated and running bin/shakapacker directly), rscConfigSupportsDiscovery() incorrectly returns false and silently falls back to the broad client-reference scan instead of enforcing the discovery manifest. That leaves rspack apps on the old behavior even though their generated configs can produce graph-derived refs.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 267d8fc988

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

};

const rscConfigSupportsDiscovery = () => {
const rscWebpackConfig = resolve('config/webpack/rscWebpackConfig.js');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check the rspack RSC config path before falling back

In rspack projects the generator writes rscWebpackConfig.js under config/rspack/ via destination_config_path, but the emitted resolver always probes config/webpack/rscWebpackConfig.js. When a current rspack RSC app has a server-component registration entry but no generated manifest (for example after cleaning ssr-generated and running bin/shakapacker directly), rscConfigSupportsDiscovery() incorrectly returns false and silently falls back to the broad client-reference scan instead of enforcing the discovery manifest. That leaves rspack apps on the old behavior even though their generated configs can produce graph-derived refs.

Useful? React with 👍 / 👎.

@justin808

Copy link
Copy Markdown
Member

Review sweep after 267d8fc: the remaining current thread is the known fileContainsAll comment-line false-positive risk and is already tracked in #3642. The branch has been rebased/merged forward after #3590/#3616/#3576, targeted local validations passed in the worker pass, and CI is now running on the new head. Resolving the thread so the only remaining gate is CI/labels.

@claude

claude Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Code Review — PR #3556: Derive Pro RSC client refs from the RSC graph

Overview

This PR wires RSC client-reference discovery into the precompile/build flow so the webpack configs use a point-in-time manifest (rsc-client-references.json) instead of broad filesystem scanning. The generator now emits a server-component-registration-entry.js and a manifest-backed rscClientReferences IIFE, the precompile hook runs a discovery build via RSCReferenceDiscoveryPlugin, and the Pro dummy is upgraded to the same contract.

The architecture is sound and the test coverage (unit, generator contract, and integration specs) is thorough. Comments below range from a real correctness concern down to style/doc nits.


Correctness

scoped_object_literal_range — range_end includes the semicolon; replacement text also ends with one

rsc_client_references_js ends with })(); (semicolon included). The range returned by scoped_object_literal_range already accounts for a trailing ; on the object literal. Replacing content[range_start...range_end] therefore produces the correct result — no double semicolon and no missing semicolon. This is correct, but the reasoning is subtle and a single targeted unit test covering replace_legacy_rsc_client_references_setup on a literal that lacks a trailing semicolon (e.g. const rscClientReferences = { ... }\n) would pin the edge case.

E2E CSS FOUC assertion loosened to accept multiple patterns

The test now accepts either a rel="preload" hint or a data-precedence stylesheet link before the probe element. The old assertion tested the specific React 19 internal serialization format precisely. The new form is CI-safe but loses the ability to catch a regression where the CSS is present in the document after the probe instead of before it. The probeIndex > stylesheetLinkIndex ordering guard partially compensates — good call including that.


Design / Architecture

Two implementations of the same resolution cascade (generator IIFE vs Pro dummy function)

rsc_client_references_js (Ruby, emits an IIFE) and rscManifestClientReferences.js (Pro dummy, a named function) must stay byte-for-byte aligned on error messages, env-var names, and fallback ordering. The contract tests in rsc_generator_spec.rb and rsc-manifest-client-references.test.js pin this — that's the right approach. The trade-off is that every future behaviour change requires touching both files and both test suites. The PR description acknowledges the follow-up (#3642) already; this comment is just a record of the ongoing maintenance cost.

require('fs') inside the generated IIFE vs at top of file

The IIFE requires fs inline, which works (Node caches modules), and the comment explains why. However, users who later inspect or edit the generated config will see two different patterns for the same standard library across their webpack config files (e.g., rscManifestClientReferences.js uses top-level const fs = require('fs') while the generated IIFE uses an inline destructure). Consider documenting the reason inline (the generator cannot guarantee fs isn't already bound at module scope under a different name) or extracting the fs destructure to the top of the injected snippet.

Path resolution inconsistency between the template and the Pro dummy

rscWebpackConfig.js.tt uses resolve(config.source_path, config.source_entry_path, '../generated/...').
The Pro dummy uses dirname(sourceEntryDirectory) to reach the same path. Both are correct; the ../ form is slightly less readable. A #3642 follow-up to unify them would reduce future confusion.


Performance

components_for_server_registration is scanned twice during generate_packs

create_server_packgenerated_server_pack_file_contentcomponents_for_server_registration (Dir.glob), and create_server_component_registration_entryserver_component_registration_entriescomponents_for_server_registration again. The staleness path already passes the pre-computed map to avoid a double scan, but generate_packs itself still triggers two globs. Already tracked in #3642 — noting it here for completeness.

fileContainsAll reads two files per rscConfigSupportsDiscovery call

In the generated IIFE (module-scope evaluation), this runs once at startup — fine. In the Pro dummy rscManifestClientReferences() (called per webpack compile), rscConfigSupportsDiscovery reads both rscWebpackConfig.js and bin/shakapacker-precompile-hook from disk on every invocation. In practice this branch is reached only when the default manifest is absent and the registration entry is present (the "old tooling" upgrade path), so it is not hot. A comment clarifying this non-hot-path status would help future readers avoid adding memoization unnecessarily.


Security

No shell injection risks. Subprocess invocations (system(env, shakapacker_bin, ...)) use the array form with a fixed binary path derived from the Rails root (validated by checking for config/environment.rb). Environment variable overrides use a hash, so no shell interpolation. Clean.


Test Coverage

Coverage is strong. A few gaps:

  • Missing: integration test that the discovery plugin actually writes rsc-client-references.json. Every test mocks the filesystem. An end-to-end test (or smoke test in the Pro dummy CI job) that runs the full RSC_REFERENCE_DISCOVERY_BUILD=true bin/shakapacker and asserts the file exists with { refs: [...] } would confirm the plugin→manifest→consumer pipeline end-to-end.

  • with_env helper in the shared spec — thoughtful implementation; the failing-iterator test is a nice touch. Minor nit: overrides.each_key / overrides.each iterates the hash twice; a single overrides.each { |k, v| original[k] = ENV.fetch(k, nil); ENV[k] = v } would be slightly cleaner (though the current split is clearer when reading alongside the failing-iterator test).


Nits

  • build:dev:watch in package.json now runs the full precompile hook on every watcher restart. For large codebases this could slow down the "tweak server component → restart watcher" loop. The PR description notes this is intentional. A --skip-discovery env var escape hatch (or a fast-path when the registration entry hasn't changed) might be a useful follow-up.

  • rsc-webpack-config.test.js reads the source file at describe-time (outside beforeEach). Fine for a static assertion; a comment like // static source analysis — no module loading needed would prevent a future reader from adding unnecessary setup.


Summary

The core approach is correct, the contract-test strategy (pinning both sides of the generator/dummy mirror) is a clever way to prevent drift, and the double recursion guard (SHAKAPACKER_SKIP_PRECOMPILE_HOOK + RSC_REFERENCE_DISCOVERY_BUILD) is sound defense-in-depth. The main actionable items:

  1. Add a unit test for replace_legacy_rsc_client_references_setup on an object literal with no trailing semicolon.
  2. Consider an integration-level assertion that the discovery build actually writes the manifest.
  3. Remaining nits (require('fs') placement, path resolution style) are good Follow up RSC manifest migration review nits #3642 candidates.

Generated with Claude Code

next unless stripped_body.match?(/\bdirectory\s*:\s*resolve\(\s*config\.source_path\s*\)/)

range_end = close_brace + 1
range_end += 1 if content[range_end] == ";"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semicolon-skipping logic here is subtle: when the original object literal ends with ;, range_end moves one past it, so the replacement text (rsc_client_references_js, which itself ends with })();) lands correctly without a double semicolon. This is correct — but the edge case where the original literal has no trailing semicolon (const rscClientReferences = { ... }\n) is untested. In that case range_end stays at close_brace + 1 and the replacement still works fine, but a unit test directly on replace_legacy_rsc_client_references_setup for both shapes (with and without ;) would prevent a future regression from going unnoticed.

Suggested change
range_end += 1 if content[range_end] == ";"
range_end = close_brace + 1
# Skip a trailing semicolon so the replacement (which already ends with `);`)
# lands without a double semicolon or a missing one.
range_end += 1 if content[range_end] == ";"
return [match.begin(0), range_end]

Comment on lines +16 to +17
overrides.each_key { |key| original[key] = ENV.fetch(key, nil) }
overrides.each { |key, value| ENV[key] = value }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two-pass iteration (first collect originals, then apply overrides) is intentional here: the "preserves setup failures while cleaning any captured env keys" test below verifies that a bad object that raises during each_key still leaves original in a defined state so the ensure block can safely iterate it. This is correct and the separation is actually important for the test.

One small nit: the comment on the original = {} line above (outside the begin) already explains the invariant; the code is clear as-is.

Comment on lines +96 to +105
function rscConfigSupportsDiscovery() {
const rscWebpackConfig = path.resolve('config/webpack/rscWebpackConfig.js');
const precompileHook = path.resolve('bin/shakapacker-precompile-hook');

return (
fileContainsAll(rscWebpackConfig, ['RSC_REFERENCE_DISCOVERY_BUILD', 'RSCReferenceDiscoveryPlugin']) &&
fileContainsAll(precompileHook, [
'generate_rsc_manifest_client_references_if_needed',
'RSC_REFERENCE_DISCOVERY_BUILD',
])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rscConfigSupportsDiscovery reads two files from disk each time it is called. Because rscManifestClientReferences is a function (called per webpack compile), this branch is reached on every invocation when the default manifest is absent but the registration entry is present — the "old tooling" upgrade path.

In practice this path should only be hit by apps that have not yet re-run rails g react_on_rails:rsc, so it is not hot. Worth a short comment so future readers don't prematurely add memoization (or, conversely, don't accidentally move this into a hot path without thinking):

Suggested change
function rscConfigSupportsDiscovery() {
const rscWebpackConfig = path.resolve('config/webpack/rscWebpackConfig.js');
const precompileHook = path.resolve('bin/shakapacker-precompile-hook');
return (
fileContainsAll(rscWebpackConfig, ['RSC_REFERENCE_DISCOVERY_BUILD', 'RSCReferenceDiscoveryPlugin']) &&
fileContainsAll(precompileHook, [
'generate_rsc_manifest_client_references_if_needed',
'RSC_REFERENCE_DISCOVERY_BUILD',
])
// Checks whether the app's RSC webpack config and precompile hook both contain the markers
// required to produce the discovery manifest. Reading the two files here is intentionally
// un-memoized: this function is only reached on the "old tooling" fallback path (registration
// entry exists, but no manifest yet), which is a one-time transition state, not a hot path.
function rscConfigSupportsDiscovery() {

Comment on lines 138 to +139
create_server_pack(verbose: verbose) if ReactOnRails.configuration.server_bundle_js_file.present?
create_server_component_registration_entry(verbose: verbose) if ReactOnRails::Utils.rsc_support_enabled?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both create_server_pack (via generated_server_pack_file_content) and create_server_component_registration_entry (via server_component_registration_entries) independently call components_for_server_registration, which runs an un-memoized Dir.glob. This means every generate_packs call with RSC enabled does two filesystem scans of the component tree.

The staleness check path (stale_or_missing_packs?) avoids this by computing the map once and passing it through. The same pattern could be applied here — compute components = components_for_server_registration once and pass it to both callers. Non-blocking since this is tracked in #3642.

const rscWebpackConfigPath = path.resolve(__dirname, '../config/webpack/rscWebpackConfig.js');

describe('rscWebpackConfig discovery build contract', () => {
const source = fs.readFileSync(rscWebpackConfigPath, 'utf8');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the source file at describe-time (outside beforeEach) is intentional here — this is a static source analysis test, not a runtime execution test. A short comment would prevent a future reader from thinking this is a mistake or adding unnecessary beforeEach setup:

Suggested change
const source = fs.readFileSync(rscWebpackConfigPath, 'utf8');
// Static analysis of the compiled source — read once at describe-time.
const source = fs.readFileSync(rscWebpackConfigPath, 'utf8');

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Pro Node Renderer Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Status
Pro Node Renderer: simple_eval (non-RSC) 2245.86 ▲3.7% (2165.9) 4.06 ▼2.9% (4.18) 5.07 ▼45.0% (9.22) 200=67382
Pro Node Renderer: react_ssr (non-RSC) 1888.36 ▼1.0% (1907.12) 4.75 ▼0.3% (4.76) 6.14 ▼33.5% (9.23) 200=56655

▲/▼ change vs baseline · 🔴 significant regression · 🟢 significant improvement (tracked measures) · (n) = baseline

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Pro (shard 2/2) Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Status
/empty: Pro 1308.17 ▲6.5% (1228.1) 6.16 ▲5.1% (5.86) 9.37 ▲5.2% (8.91) 200=39512
/ssr_shell_error: Pro 356.61 ▲7.3% (332.34) 24.78 ▲16.7% (21.23) 33.85 ▼3.2% (34.96) 200=10708
/ssr_sync_error: Pro 354.68 ▲8.0% (328.48) 21.75 ▲0.5% (21.64) 33.01 ▼10.7% (36.95) 200=10719
/rsc_component_error: Pro 301.68 ▼8.3% (328.95) 19.25 ▼11.3% (21.71) 48.72 ▲38.5% (35.18) 200=9115
/non_existing_stream_react_component: Pro 368.98 ▲7.7% (342.74) 15.31 ▼25.7% (20.6) 28.17 ▼16.9% (33.9) 200=11223
/server_side_redux_app_cached: Pro 312.81 ▼14.7% (366.81) 18.64 ▼8.4% (20.34) 36.79 ▲16.7% (31.53) 200=9453
/loadable: Pro 333.76 ▲10.8% (301.18) 23.69 ▼1.3% (24.01) 34.76 ▼1.5% (35.3) 200=10086
/apollo_graphql: Pro 154.07 ▲9.9% (140.15) 56.4 ▲11.2% (50.72) 80.03 ▼8.0% (87.02) 200=4658
/console_logs_in_async_server: Pro 3.57 ▲11.7% (3.2) 2121.04 ▼0.1% (2122.37) 2140.55 ▼2.9% (2203.8) 200=122
/stream_error_demo: Pro 347.11 ▲6.5% (325.87) 22.47 ▲5.3% (21.34) 33.76 ▼6.2% (35.98) 200=10488
/stream_async_components: Pro 348.0 ▲5.0% (331.49) 22.68 ▲3.7% (21.87) 35.92 ▲0.1% (35.89) 200=10513
/rsc_posts_page_over_http: Pro 346.55 ▲5.2% (329.47) 22.95 ▲2.8% (22.33) 36.1 ▲2.5% (35.23) 200=10469
/rsc_echo_props: Pro 197.92 ▼14.3% (231.01) 29.24 ▼7.8% (31.72) 54.67 ▲13.1% (48.32) 200=5985
/async_on_server_sync_on_client_client_render: Pro 373.86 ▲5.5% (354.33) 23.38 ▲16.0% (20.16) 32.44 ▼3.1% (33.46) 200=11295
/server_router_client_render: Pro 366.81 ▲4.6% (350.61) 21.21 ▲5.2% (20.16) 30.78 ▼6.2% (32.81) 200=11084
/unwrapped_rsc_route_stream_render: Pro 363.18 ▲8.1% (336.12) 21.45 ▲4.3% (20.56) 36.08 ▲4.4% (34.56) 200=10971
/async_render_function_returns_component: Pro 367.37 ▲11.3% (330.03) 21.82 ▲2.3% (21.32) 33.62 ▼2.8% (34.6) 200=11103
/native_metadata: Pro 354.04 ▲5.3% (336.32) 22.67 ▲2.5% (22.11) 33.71 ▼5.6% (35.73) 200=10699
/hybrid_metadata_streaming: Pro 351.79 ▲5.9% (332.31) 22.18 ▲3.7% (21.4) 33.02 ▼10.2% (36.77) 200=10634
/cache_demo: Pro 344.72 ▲6.0% (325.23) 22.56 ▲2.6% (21.98) 34.54 ▼7.8% (37.47) 200=10351
/client_side_hello_world_shared_store: Pro 357.87 ▲7.5% (332.97) 21.82 ▲1.8% (21.44) 34.13 ▲2.3% (33.37) 200=10812
/client_side_hello_world_shared_store_defer: Pro 361.48 ▲8.6% (332.88) 21.67 ▼2.4% (22.2) 33.6 ▼0.6% (33.8) 200=10923
/server_side_hello_world_shared_store_controller: Pro 304.79 ▲9.6% (278.02) 26.27 ▲0.7% (26.1) 38.02 ▼5.7% (40.34) 200=9215
/server_side_hello_world: Pro 349.82 ▲8.0% (323.98) 25.06 ▲13.1% (22.15) 33.42 ▼5.8% (35.48) 200=10569
/client_side_log_throw: Pro 321.81 ▼12.6% (368.2) 18.05 ▼9.1% (19.85) 29.11 ▼3.1% (30.04) 200=9726
/server_side_log_throw_plain_js: Pro 407.3 ▲9.1% (373.25) 19.81 ▲3.3% (19.18) 30.5 ▼2.0% (31.13) 200=12308
/server_side_log_throw_raise_invoker: Pro 423.14 ▲4.5% (405.06) 18.31 ▲6.0% (17.27) 27.01 ▼2.3% (27.65) 200=12786
/server_side_redux_app: Pro 348.18 ▲6.7% (326.31) 22.77 ▲5.5% (21.57) 32.18 ▼6.4% (34.37) 200=10521
/server_side_redux_app_cached: Pro 384.35 ▲4.8% (366.81) 22.8 ▲12.1% (20.34) 30.18 ▼4.3% (31.53) 200=11611
/render_js: Pro 392.84 ▲6.2% (369.78) 15.95 ▼18.0% (19.46) 26.77 ▼14.3% (31.22) 200=11872
/pure_component: Pro 361.14 ▲8.0% (334.41) 21.76 ▼0.8% (21.94) 31.27 ▼14.1% (36.4) 200=10912
/turbolinks_cache_disabled: Pro 389.82 ▲7.1% (364.13) 22.3 ▲11.1% (20.08) 30.21 ▼3.9% (31.43) 200=11777
/xhr_refresh: Pro 304.71 ▲9.2% (279.11) 26.2 ▲0.1% (26.18) 36.94 ▼4.0% (38.46) 200=9150
/broken_app: Pro 357.35 ▲3.1% (346.76) 22.03 ▲2.7% (21.45) 31.38 ▼8.0% (34.1) 200=10800
/server_render_with_timeout: Pro 291.36 ▼16.0% (346.97) 19.99 ▼7.4% (21.6) 26.79 ▼19.7% (33.35) 200=8806

▲/▼ change vs baseline · 🔴 significant regression · 🟢 significant improvement (tracked measures) · (n) = baseline

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Core Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Status
/: Core 2.89 ▲5.6% (2.74) 2015.99 ▼26.1% (2728.36) 3291.65 ▼10.8% (3691.41) 200=99
/client_side_hello_world: Core 457.22 ▼29.0% (644.26) 11.29 ▲21.9% (9.26) 20.69 ▼9.4% (22.83) 200=13914
/client_side_rescript_hello_world: Core 647.61 ▼1.7% (658.94) 7.08 ▼23.6% (9.27) 17.87 ▼19.9% (22.31) 200=19696
/client_side_hello_world_shared_store: Core 601.65 ▼2.4% (616.66) 11.67 ▲16.8% (9.99) 28.3 ▲21.3% (23.33) 200=18175
/client_side_hello_world_shared_store_controller: Core 448.15 ▼27.5% (618.1) 11.97 ▲21.6% (9.85) 14.35 ▼40.7% (24.21) 200=13628
/client_side_hello_world_shared_store_defer: Core 647.88 ▲3.9% (623.4) 7.15 ▼28.7% (10.03) 16.78 ▼26.7% (22.9) 200=19705
/server_side_hello_world_shared_store: Core 12.77 ▲8.9% (11.73) 492.79 ▼25.5% (661.1) 780.47 ▼8.6% (853.74) 200=395
/server_side_hello_world_shared_store_controller: Core 10.85 ▼8.4% (11.85) 481.39 ▼25.7% (647.68) 567.66 ▼33.6% (855.45) 200=342
/server_side_hello_world_shared_store_defer: Core 12.86 ▲9.8% (11.71) 644.74 ▲0.5% (641.52) 819.67 ▼5.6% (868.09) 200=395
/server_side_hello_world: Core 21.87 ▼8.0% (23.77) 249.29 ▼22.5% (321.6) 295.52 ▼26.0% (399.32) 200=673
/server_side_hello_world_hooks: Core 20.17 ▼14.8% (23.68) 297.69 ▼7.2% (320.72) 386.77 ▼3.8% (401.86) 200=617
/server_side_hello_world_props: Core 19.35 ▼18.9% (23.87) 315.31 ▼3.3% (325.97) 367.82 ▼6.6% (393.73) 200=593
/client_side_log_throw: Core 659.53 ▼1.8% (671.33) 8.93 ▼4.0% (9.3) 19.85 ▼1.5% (20.14) 200=19928
/server_side_log_throw: Core 22.08 ▼5.4% (23.35) 269.02 ▼18.9% (331.78) 440.34 ▲8.6% (405.37) 200=671
/server_side_log_throw_plain_js: Core 21.83 ▼7.2% (23.53) 255.77 ▼19.3% (316.75) 275.88 ▼31.7% (404.11) 200=673
/server_side_log_throw_raise: Core 25.92 ▲9.5% (23.66) 318.86 ▲1.1% (315.34) 386.14 ▼4.1% (402.72) 3xx=791
/server_side_log_throw_raise_invoker: Core 833.79 ▲8.2% (770.71) 7.76 ▼8.1% (8.45) 15.64 ▼6.1% (16.65) 200=25190
/server_side_hello_world_es5: Core 26.48 ▲12.2% (23.6) 310.54 ▼5.2% (327.51) 384.6 ▼4.8% (403.83) 200=804
/server_side_redux_app: Core 21.3 ▼7.4% (23.0) 245.66 ▼25.6% (330.24) 306.7 ▼24.7% (407.43) 200=654
/server_side_hello_world_with_options: Core 21.29 ▼11.2% (23.99) 217.26 ▼32.4% (321.28) 315.15 ▼20.8% (397.7) 200=653
/server_side_redux_app_cached: Core 527.99 ▼19.6% (656.33) 11.16 ▲8.3% (10.3) 15.53 ▼17.8% (18.9) 200=15952
/client_side_manual_render: Core 531.35 ▼22.1% (681.9) 9.16 ▼2.1% (9.36) 12.6 ▼36.3% (19.77) 200=16161
/render_js: Core 27.8 ▲14.3% (24.33) 238.04 ▼22.8% (308.33) 344.97 ▼10.3% (384.78) 200=848
/react_router: Core 25.25 ▲13.9% (22.17) 329.25 ▼5.8% (349.48) 404.09 ▼3.6% (419.16) 200=768
/pure_component: Core 26.86 ▲10.9% (24.23) 310.41 ▼4.5% (324.93) 377.2 ▼5.8% (400.4) 200=817
/css_modules_images_fonts_example: Core 26.34 ▲11.4% (23.65) 311.52 ▼3.2% (321.85) 388.68 ▼3.6% (403.2) 200=802
/turbolinks_cache_disabled: Core 559.41 ▼15.3% (660.64) 4.8 ▼49.5% (9.51) 12.64 ▼37.6% (20.26) 200=17015
/rendered_html: Core 26.59 ▲14.1% (23.31) 308.91 ▼3.6% (320.49) 390.21 ▼2.7% (401.02) 200=809
/xhr_refresh: Core 13.51 ▲8.8% (12.41) 612.03 ▼3.0% (630.79) 790.58 ▼7.2% (851.52) 200=416
/react_helmet: Core 25.75 ▲9.9% (23.43) 130.61 🟢 60.2% (328.55) 371.04 ▼8.0% (403.45) 200=791
/broken_app: Core 25.54 ▲8.6% (23.51) 323.86 ▼1.6% (329.14) 388.53 ▼3.0% (400.56) 200=779
/image_example: Core 20.04 ▼12.0% (22.78) 298.62 ▼8.6% (326.81) 366.1 ▼9.8% (405.88) 200=612
/turbo_frame_tag_hello_world: Core 801.03 ▲9.4% (732.06) 7.98 ▼8.2% (8.69) 16.38 ▼11.3% (18.48) 200=24201
/manual_render_test: Core 527.65 ▼21.8% (674.36) 11.02 ▲18.8% (9.28) 12.97 ▼37.0% (20.59) 200=16047

▲/▼ change vs baseline · 🔴 significant regression · 🟢 significant improvement (tracked measures) · (n) = baseline

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Pro (shard 1/2) Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Status
/: Pro 173.13 ▼1.6% (175.97) 47.75 ▲14.9% (41.57) 67.26 ▲7.2% (62.76) 200=5236
/error_scenarios_hub: Pro 326.44 ▼6.9% (350.8) 24.06 ▲20.5% (19.97) 37.15 ▲17.9% (31.52) 200=9867
/ssr_async_error: Pro 244.59 ▼24.8% (325.37) 24.02 ▲16.1% (20.68) 41.45 ▲20.8% (34.32) 200=7393
/ssr_async_prop_error: Pro 303.44 ▼5.5% (321.18) 28.9 ▲31.5% (21.98) 39.75 ▲7.5% (36.97) 200=9170
/non_existing_react_component: Pro 331.16 ▼4.0% (344.9) 19.03 ▼8.9% (20.89) 31.68 ▼11.1% (35.64) 200=10010
/non_existing_rsc_payload: Pro 335.46 ▼5.6% (355.53) 18.94 ▼6.4% (20.23) 31.4 ▼14.8% (36.87) 200=10140
/cached_react_helmet: Pro 282.72 ▼24.3% (373.23) 20.21 ▲1.5% (19.9) 25.1 ▼19.6% (31.21) 200=8546
/cached_redux_component: Pro 360.79 ▼6.5% (385.75) 21.91 ▲13.6% (19.29) 32.49 ▲2.2% (31.79) 200=10903
/lazy_apollo_graphql: Pro 143.17 ▼3.8% (148.84) 54.91 ▲13.0% (48.57) 87.01 ▲3.4% (84.18) 200=4328
/redis_receiver: Pro 81.83 ▼5.0% (86.16) 67.18 ▼0.1% (67.24) 160.85 ▲8.8% (147.88) 200=2467,3xx=9
/stream_shell_error_demo: Pro 305.2 ▼9.2% (336.11) 28.68 ▲37.6% (20.84) 39.56 ▲2.0% (38.77) 200=9224
/test_incremental_rendering: Pro 306.53 ▼9.8% (339.71) 18.3 ▼13.8% (21.23) 33.93 ▼7.5% (36.69) 200=9324
/rsc_posts_page_over_redis: Pro 91.09 ▼15.6% (107.88) 81.26 ▲29.8% (62.61) 120.33 ▲9.5% (109.87) 200=2757
/async_on_server_sync_on_client: Pro 304.31 ▼3.7% (316.06) 18.47 ▼16.7% (22.17) 34.92 ▼14.1% (40.66) 200=9200
/server_router: Pro 254.57 ▼22.8% (329.62) 22.48 ▲3.6% (21.69) 28.04 ▼21.9% (35.88) 200=7743
/unwrapped_rsc_route_client_render: Pro 308.84 ▼16.1% (368.17) 15.51 ▼21.0% (19.64) 21.89 ▼26.6% (29.81) 200=9396
/async_render_function_returns_string: Pro 308.49 ▼9.5% (340.92) 25.63 ▲22.3% (20.96) 40.11 ▲18.6% (33.81) 200=9264
/async_components_demo: Pro 184.75 ▼8.5% (201.99) 43.53 ▲21.8% (35.74) 58.41 ▲12.6% (51.87) 200=5585
/stream_native_metadata: Pro 294.08 ▼11.9% (333.66) 21.6 ▲1.1% (21.37) 35.91 ▲1.8% (35.26) 200=8892
/rsc_native_metadata: Pro 286.05 ▼12.7% (327.48) 26.82 ▲18.2% (22.69) 44.75 ▲21.7% (36.76) 200=8647
/client_side_hello_world: Pro 314.57 ▼12.7% (360.3) 24.4 ▲22.0% (20.0) 38.99 ▲26.6% (30.81) 200=9505
/client_side_hello_world_shared_store_controller: Pro 313.84 ▼7.3% (338.4) 24.62 ▲18.6% (20.76) 36.51 ▲11.5% (32.74) 200=9481
/server_side_hello_world_shared_store: Pro 251.17 ▼13.1% (288.92) 31.82 ▲23.7% (25.71) 46.34 ▲20.5% (38.46) 200=7589
/server_side_hello_world_shared_store_defer: Pro 217.23 ▼25.1% (290.16) 21.99 ▼15.6% (26.05) 31.28 ▼16.9% (37.66) 200=6609
/server_side_hello_world_hooks: Pro 330.22 ▼4.9% (347.07) 22.36 ▲6.8% (20.94) 36.2 ▲1.7% (35.59) 200=9979
/server_side_log_throw: Pro 341.55 ▼0.4% (342.92) 25.59 ▲19.0% (21.51) 33.86 ▲0.2% (33.78) 200=10323
/server_side_log_throw_raise: Pro 667.25 ▲2.2% (652.97) 11.85 ▲5.7% (11.21) 18.38 ▼0.1% (18.4) 3xx=20157
/server_side_hello_world_es5: Pro 283.38 ▼16.2% (338.16) 20.17 ▼7.5% (21.8) 24.01 ▼29.8% (34.22) 200=8620
/server_side_hello_world_with_options: Pro 329.05 ▼0.3% (329.99) 23.7 ▲9.2% (21.71) 34.72 ▲0.5% (34.54) 200=9945
/client_side_manual_render: Pro 364.98 ▲0.2% (364.29) 21.08 ▲10.1% (19.14) 31.27 ▲3.7% (30.17) 200=11032
/react_router: Pro 388.78 ▼1.5% (394.57) 19.24 ▲8.7% (17.7) 34.49 ▲15.8% (29.78) 200=11748
/css_modules_images_fonts_example: Pro 340.56 ▲0.2% (340.0) 22.96 ▲7.2% (21.43) 33.6 ▲1.3% (33.18) 200=10293
/rendered_html: Pro 278.03 ▼19.4% (345.02) 20.72 ▼2.4% (21.22) 25.03 ▼26.5% (34.07) 200=8405
/react_helmet: Pro 293.28 ▼13.3% (338.2) 29.58 ▲36.0% (21.76) 40.03 ▲19.3% (33.55) 200=8862
/image_example: Pro 301.81 ▼7.0% (324.4) 20.96 ▼4.6% (21.98) 34.25 ▼4.0% (35.67) 200=9122
/posts_page: Pro 77.88 ▼89.9% (773.23) 77.79 ▲413.4% (15.15) 121.62 ▲486.9% (20.72) 200=2362

▲/▼ change vs baseline · 🔴 significant regression · 🟢 significant improvement (tracked measures) · (n) = baseline

@justin808 justin808 removed work-in-progress codex-wip Codex is actively working this PR labels Jun 5, 2026
@justin808 justin808 merged commit 5c71c6a into main Jun 5, 2026
70 checks passed
@justin808 justin808 deleted the ihabadham/feature/pro-generate-rsc-manifest-refs branch June 5, 2026 09:59
justin808 added a commit that referenced this pull request Jun 5, 2026
* origin/main:
  Derive Pro RSC client refs from the RSC graph (#3556)
  Docs: follow up v15 anchor and Rails 5.2 flag

# Conflicts:
#	CHANGELOG.md
justin808 added a commit that referenced this pull request Jun 5, 2026
…lement

* origin/main:
  Migrate OSS dummy client examples to TypeScript (#3606)
  Docs: use async props pattern in metadata and render-function examples (#3672)
  Require PR numbers in squash merge titles (#3684)
  Improve create-app smoke diagnostics (#3599)
  Suppress regression issues when only /posts_page: Pro regresses (#3668)
  Harden ShakaPerf artifact setup
  Polish RSC generator install follow-ups
  Harden renderer teardown polish
  fix(benchmark): harden target monitoring
  Docs: clarify rails new JavaScript skip flag (#3666)
  Tighten benchmark summary annotations
  Decouple Shakapacker config helpers from SystemChecker
  Derive Pro RSC client refs from the RSC graph (#3556)
  Docs: follow up v15 anchor and Rails 5.2 flag

# Conflicts:
#	react_on_rails/spec/dummy/client/app/startup/ManualRenderApp.jsx
#	react_on_rails/spec/dummy/client/app/startup/ReduxApp.client.jsx
#	react_on_rails/spec/dummy/client/app/startup/ReduxSharedStoreApp.client.jsx
@justin808

Copy link
Copy Markdown
Member

Manual verification: reproduced the gap and confirmed the fix ✅

Reproduced via the merged OSS regression spec (spec/packs_generator_spec.rb). This PR migrates Pro RSC client-reference discovery from broad filesystem scanning to references derived from the RSC graph/manifest, and has the generator write a server-component registration entry for RSC reference discovery (Supports #3553).

Reproduction

packs_generator.rb is a hot file (also edited later by #3818), so I isolated #3556: squash-merged at 5c71c6a9c, I pinned spec/packs_generator_spec.rb to its #3556 version and reverted only the source packs_generator.rb to pre-fix (5c71c6a9c~1), then restored both to HEAD.

cd react_on_rails/spec/dummy && bundle exec rspec spec/packs_generator_spec.rb

Results

Scenario Behavior Result
Pre-fix source, #3556 spec no server-component registration entry generated; RSC-graph-derived registration/classification methods absent 13 failed / 137 (BROKEN)
Post-fix source (#3556), same spec writes the RSC registration entry, classifies server/client without rescanning, reuses precomputed entries on the staleness fast path 137 passed / 137 (FIXED)
Pro dummy rsc-manifest-client-references.test.js at HEAD manifest-backed client references config consumes the discovered refs JSON 22 passed / 22
HEAD packs_generator_spec.rb (after restore) full suite incl. later additions 158 passed / 158
# BEFORE (packs_generator.rb @5c71c6a9c~1, spec @5c71c6a9c)
✗ creates a server component registration entry for RSC reference discovery
    Failure/Error: generated_entry_content = File.read(generated_entry_path)
    (the registration entry is never written pre-fix → nothing to read)
137 examples, 13 failures

# AFTER (packs_generator.rb @5c71c6a9c, same spec)
137 examples, 0 failures

The 13 pre-fix failures are exactly the new server-component-registration / RSC-graph-classification cases.

Caveat

OSS-spec level for the generator behavior (server registration entry derivation, staleness fast path, cleanup) plus the Pro dummy config test confirming the manifest-backed client-reference helper consumes discovered refs JSON. I did not run a full Pro RSC discovery build end-to-end (bin/shakapacker-precompile-hook with a real RSC manifest) to confirm the exact client refs match the rendered graph in a live app, nor the broad-scan fallback path for apps without a generated manifest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants