Skip to content

Commit 700110e

Browse files
justin808claude
andcommitted
Merge origin/master into shakapacker-9.3.0 branch
Resolved conflicts in ESLint configuration files: - eslint-rules/no-use-client-in-server-files.cjs: Adopted master's improved regex pattern with backreferences - eslint-rules/no-use-client-in-server-files.test.cjs: Removed console.log statement - eslint.config.ts: Removed duplicate plugin definition and adopted __dirname approach The master version includes better implementations: - More robust regex pattern for detecting 'use client' directives - Cleaner code without redundant console.log - Simpler __dirname usage instead of fileURLToPath 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
2 parents ee65547 + ca6366e commit 700110e

File tree

5 files changed

+174
-44
lines changed

5 files changed

+174
-44
lines changed

eslint-rules/no-use-client-in-server-files.cjs

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,10 @@ module.exports = {
2020
description: "Prevent 'use client' directive in .server.tsx files",
2121
category: 'Best Practices',
2222
recommended: true,
23-
url: 'https://github.com/shakacode/react_on_rails/pull/1896',
23+
url: 'https://github.com/shakacode/react_on_rails/pull/1919',
2424
},
2525
messages: {
26-
useClientInServerFile:
27-
"Files with '.server.tsx' extension should not have 'use client' directive. " +
28-
'Server files are for React Server Components and should not use client-only APIs. ' +
29-
'If this component needs client-side features, rename it to .client.tsx or .tsx instead.',
26+
useClientInServerFile: `Files with '.server.tsx' extension should not have 'use client' directive. Server files are for React Server Components and should not use client-only APIs. If this component needs client-side features, rename it to .client.tsx or .tsx instead.`,
3027
},
3128
schema: [],
3229
fixable: 'code',
@@ -46,8 +43,9 @@ module.exports = {
4643
const text = sourceCode.getText();
4744

4845
// Check for 'use client' directive at the start of the file
49-
// Handle both single and double quotes, with or without semicolon
50-
const useClientPattern = /^\s*['"]use client['"];?\s*$/m;
46+
// Uses backreference (\1) to ensure matching quotes (both single or both double)
47+
// Only matches at the very beginning of the file
48+
const useClientPattern = /^\s*(['"])use client\1;?\s*\n?/;
5149
const match = text.match(useClientPattern);
5250

5351
if (match) {
@@ -58,16 +56,8 @@ module.exports = {
5856
node,
5957
messageId: 'useClientInServerFile',
6058
fix(fixer) {
61-
// Remove the 'use client' directive and any trailing newlines
62-
const start = directiveIndex;
63-
let end = directiveIndex + match[0].length;
64-
65-
// Also remove the newline after the directive if present
66-
if (text[end] === '\n') {
67-
end += 1;
68-
}
69-
70-
return fixer.removeRange([start, end]);
59+
// Remove the 'use client' directive (regex already captures trailing newline)
60+
return fixer.removeRange([directiveIndex, directiveIndex + match[0].length]);
7161
},
7262
});
7363
}

eslint-rules/no-use-client-in-server-files.test.cjs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,5 +157,3 @@ import React from 'react';
157157
},
158158
],
159159
});
160-
161-
console.log('All tests passed!');

eslint.config.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import path from 'node:path';
2-
import { fileURLToPath } from 'node:url';
32
import { globalIgnores } from 'eslint/config';
43
import jest from 'eslint-plugin-jest';
54
import prettierRecommended from 'eslint-plugin-prettier/recommended';
@@ -11,19 +10,15 @@ import js from '@eslint/js';
1110
import { FlatCompat } from '@eslint/eslintrc';
1211
import noUseClientInServerFiles from './eslint-rules/no-use-client-in-server-files.cjs';
1312

14-
const filename = fileURLToPath(import.meta.url);
15-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
16-
const dirname = path.dirname(filename) as string;
17-
1813
const compat = new FlatCompat({
19-
baseDirectory: dirname,
14+
baseDirectory: __dirname,
2015
recommendedConfig: js.configs.recommended,
2116
allConfig: js.configs.all,
2217
});
2318

2419
const config = tsEslint.config([
2520
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
26-
includeIgnoreFile(path.resolve(dirname, '.gitignore')),
21+
includeIgnoreFile(path.resolve(__dirname, '.gitignore')),
2722
globalIgnores([
2823
// compiled code
2924
'packages/*/lib/',
@@ -182,6 +177,12 @@ const config = tsEslint.config([
182177
'import/named': 'off',
183178
},
184179
},
180+
{
181+
files: ['**/*.server.ts', '**/*.server.tsx'],
182+
rules: {
183+
'react-on-rails/no-use-client-in-server-files': 'error',
184+
},
185+
},
185186
{
186187
files: ['lib/generators/react_on_rails/templates/**/*'],
187188
rules: {

lib/react_on_rails/dev/pack_generator.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "English"
4+
require "stringio"
45

56
module ReactOnRails
67
module Dev
@@ -105,7 +106,10 @@ def handle_rake_error(error, _silent)
105106
error_msg += "\n#{error.backtrace.join("\n")}" if ENV["DEBUG"]
106107

107108
# Always write to stderr, even in silent mode
108-
warn error_msg
109+
# Use STDERR constant instead of warn/$stderr to bypass capture_output redirection
110+
# rubocop:disable Style/StderrPuts, Style/GlobalStdStream
111+
STDERR.puts error_msg
112+
# rubocop:enable Style/StderrPuts, Style/GlobalStdStream
109113
end
110114

111115
def run_via_bundle_exec(silent: false)

spec/react_on_rails/dev/pack_generator_spec.rb

Lines changed: 154 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,167 @@
55

66
RSpec.describe ReactOnRails::Dev::PackGenerator do
77
describe ".generate" do
8-
it "runs pack generation successfully in verbose mode" do
9-
allow(described_class).to receive(:system)
10-
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
11-
.and_return(true)
8+
context "when in Bundler context with Rails available" do
9+
let(:mock_task) { instance_double(Rake::Task) }
10+
let(:mock_rails_app) do
11+
# rubocop:disable RSpec/VerifiedDoubles
12+
double("Rails.application").tap do |app|
13+
allow(app).to receive(:load_tasks)
14+
allow(app).to receive(:respond_to?).with(:load_tasks).and_return(true)
15+
end
16+
# rubocop:enable RSpec/VerifiedDoubles
17+
end
1218

13-
expect { described_class.generate(verbose: true) }
14-
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
19+
before do
20+
# Setup Bundler context
21+
stub_const("Bundler", Module.new)
22+
allow(ENV).to receive(:[]).and_call_original
23+
allow(ENV).to receive(:[]).with("BUNDLE_GEMFILE").and_return("/path/to/Gemfile")
24+
25+
# Setup Rails availability
26+
app = mock_rails_app
27+
rails_module = Module.new do
28+
define_singleton_method(:application) { app }
29+
define_singleton_method(:respond_to?) { |method, *| method == :application }
30+
end
31+
stub_const("Rails", rails_module)
32+
33+
# Mock Rake::Task at the boundary
34+
allow(Rake::Task).to receive(:task_defined?).with("react_on_rails:generate_packs").and_return(false)
35+
allow(Rake::Task).to receive(:[]).with("react_on_rails:generate_packs").and_return(mock_task)
36+
allow(mock_task).to receive(:reenable)
37+
allow(mock_task).to receive(:invoke)
38+
end
39+
40+
it "runs pack generation successfully in verbose mode using direct rake execution" do
41+
expect { described_class.generate(verbose: true) }
42+
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
43+
44+
expect(mock_task).to have_received(:invoke)
45+
expect(mock_rails_app).to have_received(:load_tasks)
46+
end
47+
48+
it "runs pack generation successfully in quiet mode using direct rake execution" do
49+
expect { described_class.generate(verbose: false) }
50+
.to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process
51+
52+
expect(mock_task).to have_received(:invoke)
53+
end
54+
55+
it "exits with error when pack generation fails" do
56+
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Task failed"))
57+
58+
# Mock STDERR.puts to capture output
59+
error_output = []
60+
# rubocop:disable Style/GlobalStdStream
61+
allow(STDERR).to receive(:puts) { |msg| error_output << msg }
62+
# rubocop:enable Style/GlobalStdStream
63+
64+
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
65+
expect(error_output.join("\n")).to match(/Error generating packs: Task failed/)
66+
end
67+
68+
it "outputs errors to stderr even in silent mode" do
69+
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Silent mode error"))
70+
71+
# Mock STDERR.puts to capture output
72+
error_output = []
73+
# rubocop:disable Style/GlobalStdStream
74+
allow(STDERR).to receive(:puts) { |msg| error_output << msg }
75+
# rubocop:enable Style/GlobalStdStream
76+
77+
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
78+
expect(error_output.join("\n")).to match(/Error generating packs: Silent mode error/)
79+
end
80+
81+
it "includes backtrace in error output when DEBUG env is set" do
82+
allow(ENV).to receive(:[]).with("DEBUG").and_return("true")
83+
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Debug error"))
84+
85+
# Mock STDERR.puts to capture output
86+
error_output = []
87+
# rubocop:disable Style/GlobalStdStream
88+
allow(STDERR).to receive(:puts) { |msg| error_output << msg }
89+
# rubocop:enable Style/GlobalStdStream
90+
91+
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
92+
expect(error_output.join("\n")).to match(/Error generating packs: Debug error.*pack_generator_spec\.rb/m)
93+
end
94+
95+
it "suppresses stdout in silent mode" do
96+
# Mock task to produce output
97+
allow(mock_task).to receive(:invoke) do
98+
puts "This should be suppressed"
99+
end
100+
101+
expect { described_class.generate(verbose: false) }
102+
.not_to output(/This should be suppressed/).to_stdout_from_any_process
103+
end
15104
end
16105

17-
it "runs pack generation successfully in quiet mode" do
18-
allow(described_class).to receive(:system)
19-
.with("bundle", "exec", "rake", "react_on_rails:generate_packs", out: File::NULL, err: File::NULL)
20-
.and_return(true)
106+
context "when not in Bundler context" do
107+
before do
108+
# Ensure we're not in Bundler context
109+
hide_const("Bundler") if defined?(Bundler)
110+
end
111+
112+
it "runs pack generation successfully in verbose mode using bundle exec" do
113+
allow(described_class).to receive(:system)
114+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
115+
.and_return(true)
116+
117+
expect { described_class.generate(verbose: true) }
118+
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
119+
120+
expect(described_class).to have_received(:system)
121+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
122+
end
123+
124+
it "runs pack generation successfully in quiet mode using bundle exec" do
125+
allow(described_class).to receive(:system)
126+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs",
127+
out: File::NULL, err: File::NULL)
128+
.and_return(true)
21129

22-
expect { described_class.generate(verbose: false) }
23-
.to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process
130+
expect { described_class.generate(verbose: false) }
131+
.to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process
132+
133+
expect(described_class).to have_received(:system)
134+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs",
135+
out: File::NULL, err: File::NULL)
136+
end
137+
138+
it "exits with error when pack generation fails" do
139+
allow(described_class).to receive(:system)
140+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs",
141+
out: File::NULL, err: File::NULL)
142+
.and_return(false)
143+
144+
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
145+
end
24146
end
25147

26-
it "exits with error when pack generation fails" do
27-
allow(described_class).to receive(:system)
28-
.with("bundle", "exec", "rake", "react_on_rails:generate_packs", out: File::NULL, err: File::NULL)
29-
.and_return(false)
148+
context "when Rails is not available" do
149+
before do
150+
stub_const("Bundler", Module.new)
151+
allow(ENV).to receive(:[]).and_call_original
152+
allow(ENV).to receive(:[]).with("BUNDLE_GEMFILE").and_return("/path/to/Gemfile")
153+
154+
# Rails not available
155+
hide_const("Rails") if defined?(Rails)
156+
end
157+
158+
it "falls back to bundle exec when Rails is not defined" do
159+
allow(described_class).to receive(:system)
160+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
161+
.and_return(true)
162+
163+
expect { described_class.generate(verbose: true) }
164+
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
30165

31-
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
166+
expect(described_class).to have_received(:system)
167+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
168+
end
32169
end
33170
end
34171
end

0 commit comments

Comments
 (0)