AGENTS.md
Instructions for AI coding agents working on the React on Rails codebase.
React on Rails is a Ruby gem + npm package that integrates React with Ruby on Rails, providing server-side rendering (SSR) via Node.js or ExecJS. This is a monorepo: the open-source gem lives at react_on_rails/, the npm package at packages/react-on-rails/, and the Pro package at react_on_rails_pro/.
Reusable Workflows
AGENTS.md: canonical entry point for agent instructions and workflow discovery.claude/commands/: Claude Code slash commands.agents/workflows/: shared prompt templates and reusable workflows for Codex, GPT, and other non-Claude tools- When the user asks to address PR review comments outside Claude slash commands, follow
.agents/workflows/address-review.md
Canonical Agent Policy
AGENTS.md is the canonical source for repository-wide agent rules:
- Commands and test/lint workflow
- Code style and formatting expectations
- Git/PR boundaries and safety rules
- Directory and documentation boundaries
Other agent-facing docs (for example CLAUDE.md) should contain only tool-specific workflow notes and link back here.
If there is a conflict, AGENTS.md wins.
Commands
# Install dependencies
bundle && pnpm install
# Build TypeScript → JavaScript
pnpm run build
# Lint (MANDATORY before every commit)
bundle exec rubocop # Ruby — must pass with zero offenses
pnpm run lint # JS/TS via ESLint
pnpm start format.listDifferent # Check Prettier formatting
rake lint # All linting (Ruby + JS + formatting)
# Auto-fix formatting
rake autofix # Preferred for all formatting
# Run tests
rake run_rspec:gem # Ruby unit tests (gem code)
rake run_rspec:dummy # Ruby integration tests (dummy Rails app)
pnpm run test # JavaScript/TypeScript tests
rake # Full suite (lint + all tests except examples)
# Type checking
pnpm run type-check # TypeScript
bundle exec rake rbs:validate # RBS signatures
# Additional test subsets
rake run_rspec # All Ruby tests
rake all_but_examples # All tests except generated examples
rake run_rspec:shakapacker_examples_basic # Single example test
# Full initial setup
bundle && pnpm install && rake shakapacker_examples:gen_all && rake node_package && rake
# CI/workflow linting
actionlint # GitHub Actions lint
yamllint .github/ # YAML lint (do NOT run RuboCop on .yml files)
# Dependency version updates
rake shakapacker:update_version[9.6.1] # Update shakapacker across the monorepo
Updating Shakapacker
Use rake shakapacker:update_version[VERSION] to update shakapacker across the entire monorepo. This single command updates all Gemfiles, package.json files, Gemfile.lock files, and pnpm-lock.yaml. Do not manually edit individual version references — always use the rake task to keep everything in sync.
The task handles Ruby version switching for apps that require a different Ruby version (set RUBY_VERSION_MANAGER to rvm, rbenv, asdf, or mise if needed; defaults to rvm). It continues gracefully if a single lock file update fails (e.g., due to a missing Ruby version).
Testing
- Prefer local testing over CI iteration — don't push "hopeful" fixes. Apply the 15-minute rule: if 15 more minutes of local testing would catch the issue before CI does, spend the 15 minutes.
- Never claim a test is "fixed" without running it locally first. Use "This SHOULD fix..." or "Proposed fix (UNTESTED)" for unverified changes.
- Automated tests passing is necessary but not sufficient. If your changes affect how the app starts, builds, or serves, you must also verify the dev environment manually. See Manual Dev Environment Testing for the full checklist.
- Ruby: RSpec. Unit tests in
react_on_rails/spec/react_on_rails/, integration tests via a dummy Rails app inreact_on_rails/spec/dummy/. - JavaScript/TypeScript: Jest. Tests in
packages/react-on-rails/tests/. - E2E: Playwright. Tests in
react_on_rails/spec/dummy/e2e/playwright/e2e/. Run withcd react_on_rails/spec/dummy && pnpm test:e2e. - The dummy app (
react_on_rails/spec/dummy/) is a full Rails application used for integration testing. Many tests require it.
Run specific test files:
bundle exec rspec react_on_rails/spec/react_on_rails/path/to/spec.rb
cd react_on_rails/spec/dummy && bundle exec rspec spec/path/to/spec.rb
Project Structure
| Directory | Purpose |
|---|---|
react_on_rails/lib/react_on_rails/ | Ruby gem source — helpers, configuration, SSR pool, engine |
react_on_rails/lib/generators/ | Rails generators for react_on_rails:install |
react_on_rails/spec/ | RSpec tests (unit + integration via dummy app) |
react_on_rails/spec/dummy/ | Full Rails app for integration testing and E2E |
packages/react-on-rails/src/ | TypeScript source — client-side React integration |
packages/react-on-rails/tests/ | Jest tests for the npm package |
react_on_rails_pro/ | Pro package (separate gem + npm, own lint config) |
rakelib/ | Rake task definitions |
docs/oss/ | OSS documentation — published to the ShakaCode website |
docs/pro/ | Pro documentation — installation, configuration, RSC, node renderer, caching |
internal/contributor-info/ | Internal contributor docs (not published to the website) |
internal/planning/ | Internal planning docs, drafts, and historical analysis |
internal/react_on_rails_pro/contributors-info/ | Internal Pro contributor docs (not published to the website) |
analysis/ | Investigation and analysis documents (kebab-case .md files) |
Code Style
Ruby (RuboCop)
Line length max 120 characters. Run bundle exec rubocop [file] to check.
Line length — break long chains:
# Bad
content = pack_content.gsub(/import.*from.*['"];/, "").gsub(/ReactOnRails\.register.*/, "")
# Good
content = pack_content.gsub(/import.*from.*['"];/, "")
.gsub(/ReactOnRails\.register.*/, "")
Named subjects in RSpec:
# Bad
subject { instance.method_name(arg) }
# Good
subject(:method_result) { instance.method_name(arg) }
Security violations — scope disable comments tightly:
# rubocop:disable Security/Eval
expect { evaluate(sanitized_content) }.not_to raise_error
# rubocop:enable Security/Eval
JavaScript/TypeScript
Prettier handles all formatting. Never manually format — run rake autofix instead.
Git Workflow
Branch naming: type/descriptive-name (e.g., fix/ssr-hydration-mismatch)
Commit messages: Explain why, not what. One logical change per commit.
PR creation: Use gh pr create with a clear title, summary, and test plan.
Review Workflow
For small, focused PRs (roughly 5 files changed or fewer and one clear purpose):
- Use at most one AI reviewer that leaves inline comments. Additional AI tools should be summary-only or used manually.
- Wait for the first full review pass to finish before pushing follow-up commits.
- Batch review fixes into one follow-up push when practical. Do not create a new commit for each minor comment.
- Treat as blocking only: correctness bugs, failing tests, regressions, and clear inconsistencies with adjacent code. Nits and style suggestions are optional unless a maintainer asks for them.
- Verify language, runtime, and library claims locally before changing code in response to AI review comments.
- Deduplicate repeated bot comments before acting on them. Fix the underlying issue once, then resolve the duplicates.
- Rebase or merge
mainonce, near the end of the review cycle. ForCHANGELOG.mdconflicts, prefer resolving them as the final step before merge. - When asking an agent to address review comments, instruct it to classify comments into
blocking,optional, andnoise, then apply only theblockingitems plus any explicitly selected optional items.
Boundaries
Always
- Run
bundle exec rubocopbefore committing — CI will reject violations - Use
pnpmfor all JS operations — nevernpmoryarn - Use
bundle execfor Ruby commands - Ensure all files end with a newline
- Let Prettier and RuboCop handle formatting — never format manually
- When adding docs under
docs/oss/ordocs/pro/, also add the doc ID todocs/sidebars.ts— CI will fail otherwise. To intentionally exclude a doc from the sidebar, add its ID todocs/.sidebar-exclusionswith a reason comment.
Ask First
- Destructive git operations (force push, reset --hard, branch deletion)
- Changes to CI workflows (
.github/workflows/) - Changes to build configuration (
package.jsonscripts, webpack config) - Modifying the Pro package (
react_on_rails_pro/)
Never
- Skip pre-commit hooks (
--no-verify) - Commit secrets, credentials, or
.envfiles - Commit
package-lock.json,yarn.lock, or other non-pnpm lock files - Add files to the
docs/root — OSS docs go indocs/oss/subdirectories (getting-started/,core-concepts/,building-features/,configuration/,api-reference/,deployment/,migrating/,upgrading/,misc/); Pro docs go indocs/pro/ - Force push to
mainormaster
Key Concept: File Suffixes vs. RSC Directive
React on Rails has two independent systems that both use "client" and "server" terminology. Do not confuse them.
1. Bundle Placement (.client. / .server. file suffixes)
A React on Rails auto-bundling feature that controls which webpack bundle imports a file. This exists independently of React Server Components and is used with or without RSC:
Component.client.jsx→ imported only in the client bundle (browser)Component.server.jsx→ imported only in the server bundle (and RSC bundle when RSC enabled)Component.jsx(no suffix) → imported in both bundles
This controls where the source file is loaded, nothing more. A .server.jsx file is NOT a React Server Component — it is simply a file that webpack includes in the server bundle (and the RSC bundle when RSC is enabled). These suffixes only make sense for client components, as server components exist only in the RSC bundle.
2. RSC Classification ('use client' directive)
The 'use client' directive is part of the React Server Components architecture. It marks a component as a React Client Component. Components without it are treated as React Server Components.
When auto-bundling is enabled with RSC support (Pro feature), React on Rails uses this directive to control:
- Registration:
'use client'→ReactOnRails.register(), no'use client'→registerServerComponent() - RSC bundling: The RSC webpack loader uses this directive to decide whether a component is included in the RSC bundle or replaced with a client reference in that bundle
The client_entrypoint? method in packs_generator.rb checks for this directive.
They Are Orthogonal
A .client.jsx file can be a React Server Component (if it lacks 'use client'), and a .server.jsx file can be a React Client Component (if it has 'use client'). In practice, paired .client./.server. files should have consistent 'use client' status because the client and server must agree on the component's RSC role for hydration to work.
Changelog
Update /CHANGELOG.md for user-visible changes only (features, bug fixes, breaking changes, deprecations, performance improvements). Do not add entries for linting, formatting, refactoring, tests, or doc fixes.
- Format:
[PR 1818](https://github.com/shakacode/react_on_rails/pull/1818) by [username](https://github.com/username)(no hash before PR number) - Pro-only changes use an inline
**[Pro]**tag prefix within the standard category sections (e.g.,- **[Pro]** **Feature name**: Description...); do NOT create separate#### Prosubsections