Skip to content
Merged
14 changes: 11 additions & 3 deletions lib/react_on_rails/packs_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module ReactOnRails
# rubocop:disable Metrics/ClassLength
class PacksGenerator
CONTAINS_CLIENT_OR_SERVER_REGEX = /\.(server|client)($|\.)/
COMPONENT_EXTENSIONS = /\.(jsx?|tsx?)$/
MINIMUM_SHAKAPACKER_VERSION = "6.5.1"

def self.instance
Expand Down Expand Up @@ -228,14 +229,20 @@ def component_name_to_path(paths)
paths.to_h { |path| [component_name(path), path] }
end

def filter_component_files(paths)
paths.grep(COMPONENT_EXTENSIONS)
end

def common_component_to_path
common_components_paths = Dir.glob("#{components_search_path}/*").grep_v(CONTAINS_CLIENT_OR_SERVER_REGEX)
component_name_to_path(common_components_paths)
filtered_paths = filter_component_files(common_components_paths)
component_name_to_path(filtered_paths)
end

def client_component_to_path
client_render_components_paths = Dir.glob("#{components_search_path}/*.client.*")
client_specific_components = component_name_to_path(client_render_components_paths)
filtered_client_paths = filter_component_files(client_render_components_paths)
client_specific_components = component_name_to_path(filtered_client_paths)

duplicate_components = common_component_to_path.slice(*client_specific_components.keys)
duplicate_components.each_key { |component| raise_client_component_overrides_common(component) }
Expand All @@ -245,7 +252,8 @@ def client_component_to_path

def server_component_to_path
server_render_components_paths = Dir.glob("#{components_search_path}/*.server.*")
server_specific_components = component_name_to_path(server_render_components_paths)
filtered_server_paths = filter_component_files(server_render_components_paths)
server_specific_components = component_name_to_path(filtered_server_paths)

duplicate_components = common_component_to_path.slice(*server_specific_components.keys)
duplicate_components.each_key { |component| raise_server_component_overrides_common(component) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import styles from './ComponentWithCSSModule.module.css';

const ComponentWithCSSModule = () => {
return (
<div className={styles.container}>
<h1 className={styles.title}>Hello from CSS Module Component</h1>
</div>
);
};

export default ComponentWithCSSModule;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* CSS Module for testing */
.container {
padding: 1rem;
}

.title {
font-size: 2rem;
font-weight: bold;
}
70 changes: 70 additions & 0 deletions spec/dummy/spec/packs_generator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,38 @@ def create_new_component(name)
end
end

context "when component with CSS module" do
let(:component_name) { "ComponentWithCSSModule" }
let(:component_pack) { "#{generated_directory}/#{component_name}.js" }

before do
stub_packer_source_path(component_name: component_name,
packer_source_path: packer_source_path)
described_class.instance.generate_packs_if_stale
end

it "generates a pack with valid JavaScript variable names" do
expect(File.exist?(component_pack)).to be(true)
pack_content = File.read(component_pack)

# Check that the generated pack content is valid JavaScript
expect(pack_content).to include("import ReactOnRails from 'react-on-rails/client';")
expect(pack_content).to include("import #{component_name} from")
expect(pack_content).to include("ReactOnRails.register({#{component_name}});")

# Verify that variable names don't contain dots (invalid in JS)
expect(pack_content).not_to match(/ComponentWithCSSModule\.module/)
expect(pack_content).not_to match(/import .+\.module/)
end

it "generates valid JavaScript that can be parsed without syntax errors" do
pack_content = File.read(component_pack)

# This would fail if the generated JavaScript has syntax errors
expect { eval(pack_content.gsub(/import.*from.*['"];/, "").gsub(/ReactOnRails\.register.*/, "")) }.not_to raise_error
end
end
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove eval; address RuboCop Security/Eval and line length

The Ruby eval here is unsafe and unnecessary; the prior expectations already validate the JS shape and identifiers.

Apply this diff to delete the example:

-      it "generates valid JavaScript that can be parsed without syntax errors" do
-        pack_content = File.read(component_pack)
-        
-        # This would fail if the generated JavaScript has syntax errors
-        expect { eval(pack_content.gsub(/import.*from.*['"];/, "").gsub(/ReactOnRails\.register.*/, "")) }.not_to raise_error
-      end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it "generates valid JavaScript that can be parsed without syntax errors" do
pack_content = File.read(component_pack)
# This would fail if the generated JavaScript has syntax errors
expect { eval(pack_content.gsub(/import.*from.*['"];/, "").gsub(/ReactOnRails\.register.*/, "")) }.not_to raise_error
end
end
🧰 Tools
🪛 GitHub Actions: Lint JS and Ruby

[error] 464-464: RuboCop: Layout/TrailingWhitespace: Trailing whitespace detected.


[error] 466-466: RuboCop: Security/Eval: The use of eval is a serious security risk.


[error] 466-466: RuboCop: Layout/LineLength: Line is too long. [125/120].

🤖 Prompt for AI Agents
In spec/dummy/spec/packs_generator_spec.rb around lines 462 to 468, remove the
entire "it" example that calls eval on pack_content (the block that reads
pack_content and expects no error from eval) to address RuboCop Security/Eval
and line-length concerns; simply delete those lines (and any now-unnecessary
empty lines) so the prior expectations remain as the validation for the
generated JavaScript.


def generated_server_bundle_file_path
described_class.instance.send(:generated_server_bundle_file_path)
end
Expand Down Expand Up @@ -658,6 +690,44 @@ def stub_packer_source_path(packer_source_path:, component_name:)
end
end

describe "#component_name" do
subject { described_class.instance.send(:component_name, file_path) }

context "with regular component file" do
let(:file_path) { "/path/to/MyComponent.jsx" }

it { is_expected.to eq "MyComponent" }
end

context "with client component file" do
let(:file_path) { "/path/to/MyComponent.client.jsx" }

it { is_expected.to eq "MyComponent" }
end

context "with server component file" do
let(:file_path) { "/path/to/MyComponent.server.jsx" }

it { is_expected.to eq "MyComponent" }
end

context "with CSS module file" do
let(:file_path) { "/path/to/HeavyMarkdownEditor.module.css" }

# CSS modules should still work with component_name method, but they
# should not be processed as React components by the generator
it "returns name with dot for CSS modules" do
expect(subject).to eq "HeavyMarkdownEditor.module"
end
end

context "with TypeScript component file" do
let(:file_path) { "/path/to/MyComponent.tsx" }

it { is_expected.to eq "MyComponent" }
end
end

describe "#client_entrypoint?" do
subject { described_class.instance.send(:client_entrypoint?, "dummy_path.js") }

Expand Down
Loading