Skip to content

Commit 84d57b0

Browse files
justin808claude
andcommitted
Fix CSS module handling in pack generator
Fixes #1768 - CSS module files (like HeavyMarkdownEditor.module.css) were being processed as React components, creating invalid JavaScript variable names with dots that caused build failures. Changes: - Add COMPONENT_EXTENSIONS regex to filter valid React component files - Add filter_component_files method to exclude non-component files - Update common/client/server component path methods to use filtering - Add comprehensive tests for CSS module handling - Ensure only .js, .jsx, .ts, .tsx files are processed as components This prevents CSS modules and other non-React files from being treated as components while maintaining backward compatibility. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 24826f0 commit 84d57b0

File tree

4 files changed

+102
-3
lines changed

4 files changed

+102
-3
lines changed

lib/react_on_rails/packs_generator.rb

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module ReactOnRails
66
# rubocop:disable Metrics/ClassLength
77
class PacksGenerator
88
CONTAINS_CLIENT_OR_SERVER_REGEX = /\.(server|client)($|\.)/
9+
COMPONENT_EXTENSIONS = /\.(jsx?|tsx?)$/
910
MINIMUM_SHAKAPACKER_VERSION = "6.5.1"
1011

1112
def self.instance
@@ -228,14 +229,20 @@ def component_name_to_path(paths)
228229
paths.to_h { |path| [component_name(path), path] }
229230
end
230231

232+
def filter_component_files(paths)
233+
paths.grep(COMPONENT_EXTENSIONS)
234+
end
235+
231236
def common_component_to_path
232237
common_components_paths = Dir.glob("#{components_search_path}/*").grep_v(CONTAINS_CLIENT_OR_SERVER_REGEX)
233-
component_name_to_path(common_components_paths)
238+
filtered_paths = filter_component_files(common_components_paths)
239+
component_name_to_path(filtered_paths)
234240
end
235241

236242
def client_component_to_path
237243
client_render_components_paths = Dir.glob("#{components_search_path}/*.client.*")
238-
client_specific_components = component_name_to_path(client_render_components_paths)
244+
filtered_client_paths = filter_component_files(client_render_components_paths)
245+
client_specific_components = component_name_to_path(filtered_client_paths)
239246

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

246253
def server_component_to_path
247254
server_render_components_paths = Dir.glob("#{components_search_path}/*.server.*")
248-
server_specific_components = component_name_to_path(server_render_components_paths)
255+
filtered_server_paths = filter_component_files(server_render_components_paths)
256+
server_specific_components = component_name_to_path(filtered_server_paths)
249257

250258
duplicate_components = common_component_to_path.slice(*server_specific_components.keys)
251259
duplicate_components.each_key { |component| raise_server_component_overrides_common(component) }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react';
2+
import styles from './ComponentWithCSSModule.module.css';
3+
4+
const ComponentWithCSSModule = () => {
5+
return (
6+
<div className={styles.container}>
7+
<h1 className={styles.title}>Hello from CSS Module Component</h1>
8+
</div>
9+
);
10+
};
11+
12+
export default ComponentWithCSSModule;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/* CSS Module for testing */
2+
.container {
3+
padding: 1rem;
4+
}
5+
6+
.title {
7+
font-size: 2rem;
8+
font-weight: bold;
9+
}

spec/dummy/spec/packs_generator_spec.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,38 @@ def create_new_component(name)
435435
end
436436
end
437437

438+
context "when component with CSS module" do
439+
let(:component_name) { "ComponentWithCSSModule" }
440+
let(:component_pack) { "#{generated_directory}/#{component_name}.js" }
441+
442+
before do
443+
stub_packer_source_path(component_name: component_name,
444+
packer_source_path: packer_source_path)
445+
described_class.instance.generate_packs_if_stale
446+
end
447+
448+
it "generates a pack with valid JavaScript variable names" do
449+
expect(File.exist?(component_pack)).to be(true)
450+
pack_content = File.read(component_pack)
451+
452+
# Check that the generated pack content is valid JavaScript
453+
expect(pack_content).to include("import ReactOnRails from 'react-on-rails/client';")
454+
expect(pack_content).to include("import #{component_name} from")
455+
expect(pack_content).to include("ReactOnRails.register({#{component_name}});")
456+
457+
# Verify that variable names don't contain dots (invalid in JS)
458+
expect(pack_content).not_to match(/ComponentWithCSSModule\.module/)
459+
expect(pack_content).not_to match(/import .+\.module/)
460+
end
461+
462+
it "generates valid JavaScript that can be parsed without syntax errors" do
463+
pack_content = File.read(component_pack)
464+
465+
# This would fail if the generated JavaScript has syntax errors
466+
expect { eval(pack_content.gsub(/import.*from.*['"];/, "").gsub(/ReactOnRails\.register.*/, "")) }.not_to raise_error
467+
end
468+
end
469+
438470
def generated_server_bundle_file_path
439471
described_class.instance.send(:generated_server_bundle_file_path)
440472
end
@@ -658,6 +690,44 @@ def stub_packer_source_path(packer_source_path:, component_name:)
658690
end
659691
end
660692

693+
describe "#component_name" do
694+
subject { described_class.instance.send(:component_name, file_path) }
695+
696+
context "with regular component file" do
697+
let(:file_path) { "/path/to/MyComponent.jsx" }
698+
699+
it { is_expected.to eq "MyComponent" }
700+
end
701+
702+
context "with client component file" do
703+
let(:file_path) { "/path/to/MyComponent.client.jsx" }
704+
705+
it { is_expected.to eq "MyComponent" }
706+
end
707+
708+
context "with server component file" do
709+
let(:file_path) { "/path/to/MyComponent.server.jsx" }
710+
711+
it { is_expected.to eq "MyComponent" }
712+
end
713+
714+
context "with CSS module file" do
715+
let(:file_path) { "/path/to/HeavyMarkdownEditor.module.css" }
716+
717+
# CSS modules should still work with component_name method, but they
718+
# should not be processed as React components by the generator
719+
it "returns name with dot for CSS modules" do
720+
expect(subject).to eq "HeavyMarkdownEditor.module"
721+
end
722+
end
723+
724+
context "with TypeScript component file" do
725+
let(:file_path) { "/path/to/MyComponent.tsx" }
726+
727+
it { is_expected.to eq "MyComponent" }
728+
end
729+
end
730+
661731
describe "#client_entrypoint?" do
662732
subject { described_class.instance.send(:client_entrypoint?, "dummy_path.js") }
663733

0 commit comments

Comments
 (0)