Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ After a release, run `/update-changelog` in Claude Code to analyze commits, writ

#### Fixed

- **[Pro]** **Doctor detects more stale Pro migration imports**: `react_on_rails:doctor` now warns when Pro apps still reference the base package through Jest/Vitest mock helpers or TypeScript `declare module` blocks, catching migration leftovers that import/require scanning missed. See [Issue 3104](https://github.com/shakacode/react_on_rails/issues/3104). [PR 3232](https://github.com/shakacode/react_on_rails/pull/3232) by [justin808](https://github.com/justin808).
- **[Pro]** **Node renderer now exposes `performance` when `supportModules: true`**: React 19's development build of `React.lazy` calls `performance.now()`, which previously threw `ReferenceError: performance is not defined` inside the node renderer's VM context unless users manually added `performance` via `additionalContext`. `performance` is now included in the default globals alongside `Buffer`, `process`, etc. Fixes [Issue 3154](https://github.com/shakacode/react_on_rails/issues/3154). [PR 3158](https://github.com/shakacode/react_on_rails/pull/3158) by [justin808](https://github.com/justin808).
- **Client startup now recovers if initialization begins during `interactive` after `DOMContentLoaded` already fired**: React on Rails now still initializes the page when the client bundle starts in the browser timing window after `DOMContentLoaded` but before the document reaches `complete`. Fixes [Issue 3150](https://github.com/shakacode/react_on_rails/issues/3150). [PR 3151](https://github.com/shakacode/react_on_rails/pull/3151) by [ihabadham](https://github.com/ihabadham).
- **Doctor accepts TypeScript server bundle entrypoints**: `react_on_rails:doctor` now resolves common source entrypoint suffixes (`.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`) before warning that the server bundle is missing, preventing false positives when apps use `server-bundle.ts`. [PR 3111](https://github.com/shakacode/react_on_rails/pull/3111) by [justin808](https://github.com/justin808).
Expand Down
9 changes: 7 additions & 2 deletions react_on_rails/lib/react_on_rails/doctor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2752,8 +2752,10 @@ def check_pro_renderer_mode
# caching, RSC), and may cause "component not registered" errors at runtime.
BASE_PACKAGE_IMPORT_PATTERN = %r{\bfrom\s+['"]react-on-rails(?:/[^'"]*)?['"]}
BASE_PACKAGE_REQUIRE_PATTERN = %r{\brequire\s*\(\s*['"]react-on-rails(?:/[^'"]*)?['"]\s*\)}
BASE_PACKAGE_MOCK_PATTERN = %r{\b\w+\.(?:mock|unmock|doMock|dontMock|requireActual|requireMock)\s*\(\s*['"]react-on-rails(?:/[^'"]*)?['"]} # rubocop:disable Layout/LineLength
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 # rubocop:disable Layout/LineLength is needed because of the long method list inline. A small readability improvement would be to extract the alternation into a named group or split across assignment lines:

BASE_PACKAGE_MOCK_METHODS = %r{mock|unmock|doMock|dontMock|requireActual|requireMock}
BASE_PACKAGE_MOCK_PATTERN =
  %r{\b\w+\.(?:#{BASE_PACKAGE_MOCK_METHODS.source})\s*\(\s*['"]react-on-rails(?:/[^'"]*)?['"]}

Not a blocker, just worth considering if this constant ever needs extending again.

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 inline rubocop:disable Layout/LineLength can be avoided with Ruby's extended-regex flag (x), which allows whitespace and comments inside the literal:

Suggested change
BASE_PACKAGE_MOCK_PATTERN = %r{\b\w+\.(?:mock|unmock|doMock|dontMock|requireActual|requireMock)\s*\(\s*['"]react-on-rails(?:/[^'"]*)?['"]} # rubocop:disable Layout/LineLength
BASE_PACKAGE_MOCK_PATTERN = %r{
\b\w+\. # object receiver (jest / vi / module / …)
(?:mock|unmock|doMock|dontMock|requireActual|requireMock)
\s*\(\s*['"]react-on-rails(?:/[^'"]*)?['"]
}x

Side note: requireActual('react-on-rails') is used specifically to bypass a mock and load the real base package — that's a different situation from a forgotten jest.mock. Worth deciding intentionally whether to flag it (the current behaviour) or exclude it.

BASE_PACKAGE_DECLARE_MODULE_PATTERN = %r{^\s*declare\s+module\s+['"]react-on-rails(?:/[^'"]*)?['"]}
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 ^ anchor here works in Ruby because ^ matches the beginning of a line (not just the string), so multiline file content is handled correctly. Worth a brief comment so a future reader doesn't mistake it for \A:

Suggested change
BASE_PACKAGE_DECLARE_MODULE_PATTERN = %r{^\s*declare\s+module\s+['"]react-on-rails(?:/[^'"]*)?['"]}
BASE_PACKAGE_DECLARE_MODULE_PATTERN = %r{^\s*declare\s+module\s+['"]react-on-rails(?:/[^'"]*)?['"]} # ^ = start of any line


def check_base_package_imports # rubocop:disable Metrics/CyclomaticComplexity
def check_base_package_imports # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
source_path = resolve_js_source_path
js_extensions = %w[js jsx ts tsx]
js_patterns = js_extensions.map { |ext| "#{source_path}/**/*.#{ext}" }
Expand All @@ -2762,7 +2764,10 @@ def check_base_package_imports # rubocop:disable Metrics/CyclomaticComplexity
js_patterns.each do |pattern|
Dir.glob(pattern).each do |file|
content = File.read(file)
next unless content.match?(BASE_PACKAGE_IMPORT_PATTERN) || content.match?(BASE_PACKAGE_REQUIRE_PATTERN)
next unless content.match?(BASE_PACKAGE_IMPORT_PATTERN) ||
content.match?(BASE_PACKAGE_REQUIRE_PATTERN) ||
content.match?(BASE_PACKAGE_MOCK_PATTERN) ||
content.match?(BASE_PACKAGE_DECLARE_MODULE_PATTERN)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning message misleads for mock and declaration matches

Low Severity

The new BASE_PACKAGE_MOCK_PATTERN and BASE_PACKAGE_DECLARE_MODULE_PATTERN now trigger the existing warning that says "Found imports from 'react-on-rails'" with a fix suggesting import ReactOnRails from 'react-on-rails-pro'. When the actual match is a jest.mock('react-on-rails') or declare module 'react-on-rails', calling these "imports" is inaccurate and the import-specific fix guidance doesn't apply to those cases. The diagnostic will point users to the correct files but give them inapplicable remediation advice.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fa3c407. Configure here.

Comment on lines 2758 to +2770
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 growing number of patterns and the two rubocop:disable overrides are a signal to extract a small helper. An array-based approach eliminates the complexity flags without requiring any linter suppression:

Suggested change
PATTERNS = [
BASE_PACKAGE_IMPORT_PATTERN,
BASE_PACKAGE_REQUIRE_PATTERN,
BASE_PACKAGE_MOCK_PATTERN,
BASE_PACKAGE_DECLARE_MODULE_PATTERN,
].freeze
def check_base_package_imports
source_path = resolve_js_source_path
js_extensions = %w[js jsx ts tsx]
js_patterns = js_extensions.map { |ext| "#{source_path}/**/*.#{ext}" }
files_with_base_import = []
js_patterns.each do |pattern|
Dir.glob(pattern).each do |file|
content = File.read(file)
next unless PATTERNS.any? { |p| content.match?(p) }
files_with_base_import << file
end
end

This also makes it trivial to add a fifth pattern in the future without touching the loop logic or bumping the complexity metrics.


files_with_base_import << file
end
Expand Down
38 changes: 38 additions & 0 deletions react_on_rails/spec/lib/react_on_rails/doctor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2530,6 +2530,44 @@ class << self
end
end

context "when JS tests mock the base package after a Pro migration" do
around do |example|
Dir.mktmpdir do |tmpdir|
Dir.chdir(tmpdir) do
FileUtils.mkdir_p("app/javascript/packs")
File.write("app/javascript/packs/app.test.ts",
"jest.mock('react-on-rails', () => ({ authenticityHeaders: jest.fn() }));\n")
example.run
end
end
end

it "reports warning" do
doctor.send(:check_base_package_imports)
warning_msgs = checker.messages.select { |m| m[:type] == :warning }
expect(warning_msgs.any? { |m| m[:content].include?("app.test.ts") }).to be true
end
Comment on lines +2533 to +2549
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.

Two gaps worth adding:

  1. Negative testjest.mock('react-on-rails-pro', ...) should not trigger a warning. The regex is constructed correctly (the -pro suffix prevents a match), but a negative spec locks that in explicitly.

  2. Subpath mockjest.mock('react-on-rails/client') is another common leftover pattern; a spec confirming it is caught would match the subpath coverage already present for import/require.

context "when JS tests mock the base package subpath (e.g. react-on-rails/client)" do
  # ...
  File.write("app/javascript/packs/app.test.ts",
             "jest.mock('react-on-rails/client', () => ({}));\n")
  # expects warning
end

context "when JS tests mock the Pro package (no warning expected)" do
  # ...
  File.write("app/javascript/packs/app.test.ts",
             "jest.mock('react-on-rails-pro', () => ({}));\n")
  # expects success, not warning
end

end

context "when TypeScript declaration files augment the base package after a Pro migration" do
around do |example|
Dir.mktmpdir do |tmpdir|
Dir.chdir(tmpdir) do
FileUtils.mkdir_p("app/javascript/types")
File.write("app/javascript/types/react-on-rails.d.ts",
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.

.d.ts files are picked up because **/*.ts matches anything ending in .tsreact-on-rails.d.ts qualifies. This works correctly, but it's non-obvious. A short comment in the test (or a note alongside js_extensions in doctor.rb) would help a future reader who wonders why .d.ts files are covered without an explicit extension entry.

"declare module 'react-on-rails' {\n export function register(): void;\n}\n")
example.run
end
end
end

it "reports warning" do
doctor.send(:check_base_package_imports)
warning_msgs = checker.messages.select { |m| m[:type] == :warning }
expect(warning_msgs.any? { |m| m[:content].include?("react-on-rails.d.ts") }).to be true
end
end

context "when JS files correctly import from 'react-on-rails-pro'" do
around do |example|
Dir.mktmpdir do |tmpdir|
Expand Down
Loading