Skip to content

Commit a32ebff

Browse files
justin808claude
andcommitted
Add test coverage and improve documentation
Test Coverage: - Added unit tests for bundlerUtils.js (6 tests, all passing) - Added RSpec integration test for bundler switching - Verified both Webpack and Rspack can build successfully Documentation Improvements: - Enhanced SWC config comments explaining why classic React runtime is used - Improved serverWebpackConfig error message to show: * Expected file path with actual config values * Current source_path and source_entry_path * Step-by-step verification checklist All tests passing (14 total: 4 test suites, 14 tests). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 0ab9eac commit a32ebff

File tree

4 files changed

+191
-4
lines changed

4 files changed

+191
-4
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
* Unit tests for bundlerUtils.js
3+
* Tests bundler auto-detection and helper functions
4+
*
5+
* Note: These tests verify the bundler selection logic without actually
6+
* loading Rspack (which requires Node.js globals not available in jsdom).
7+
*/
8+
9+
// Mock the bundler packages to avoid loading them
10+
jest.mock('webpack', () => ({
11+
ProvidePlugin: class MockProvidePlugin {},
12+
optimize: { LimitChunkCountPlugin: class MockLimitChunkCount {} },
13+
}));
14+
15+
jest.mock('@rspack/core', () => ({
16+
ProvidePlugin: class MockRspackProvidePlugin {},
17+
CssExtractRspackPlugin: class MockCssExtractRspackPlugin {},
18+
optimize: { LimitChunkCountPlugin: class MockRspackLimitChunkCount {} },
19+
}));
20+
21+
jest.mock('mini-css-extract-plugin', () => class MiniCssExtractPlugin {});
22+
23+
describe('bundlerUtils', () => {
24+
let mockConfig;
25+
26+
beforeEach(() => {
27+
// Reset module cache
28+
jest.resetModules();
29+
30+
// Create fresh mock config
31+
mockConfig = { assets_bundler: 'webpack' };
32+
});
33+
34+
afterEach(() => {
35+
jest.clearAllMocks();
36+
});
37+
38+
describe('getBundler()', () => {
39+
it('returns webpack when assets_bundler is webpack', () => {
40+
mockConfig.assets_bundler = 'webpack';
41+
jest.doMock('shakapacker', () => ({ config: mockConfig }));
42+
const utils = require('../../../config/webpack/bundlerUtils');
43+
44+
const bundler = utils.getBundler();
45+
46+
expect(bundler).toBeDefined();
47+
expect(bundler.ProvidePlugin).toBeDefined();
48+
expect(bundler.ProvidePlugin.name).toBe('MockProvidePlugin');
49+
});
50+
51+
it('returns rspack when assets_bundler is rspack', () => {
52+
mockConfig.assets_bundler = 'rspack';
53+
jest.doMock('shakapacker', () => ({ config: mockConfig }));
54+
const utils = require('../../../config/webpack/bundlerUtils');
55+
56+
const bundler = utils.getBundler();
57+
58+
expect(bundler).toBeDefined();
59+
// Rspack has CssExtractRspackPlugin
60+
expect(bundler.CssExtractRspackPlugin).toBeDefined();
61+
expect(bundler.CssExtractRspackPlugin.name).toBe('MockCssExtractRspackPlugin');
62+
});
63+
});
64+
65+
describe('isRspack()', () => {
66+
it('returns false when assets_bundler is webpack', () => {
67+
mockConfig.assets_bundler = 'webpack';
68+
jest.doMock('shakapacker', () => ({ config: mockConfig }));
69+
const utils = require('../../../config/webpack/bundlerUtils');
70+
71+
expect(utils.isRspack()).toBe(false);
72+
});
73+
74+
it('returns true when assets_bundler is rspack', () => {
75+
mockConfig.assets_bundler = 'rspack';
76+
jest.doMock('shakapacker', () => ({ config: mockConfig }));
77+
const utils = require('../../../config/webpack/bundlerUtils');
78+
79+
expect(utils.isRspack()).toBe(true);
80+
});
81+
});
82+
83+
describe('getCssExtractPlugin()', () => {
84+
it('returns mini-css-extract-plugin when using webpack', () => {
85+
mockConfig.assets_bundler = 'webpack';
86+
jest.doMock('shakapacker', () => ({ config: mockConfig }));
87+
const utils = require('../../../config/webpack/bundlerUtils');
88+
89+
const plugin = utils.getCssExtractPlugin();
90+
91+
expect(plugin).toBeDefined();
92+
expect(plugin.name).toBe('MiniCssExtractPlugin');
93+
});
94+
95+
it('returns CssExtractRspackPlugin when using rspack', () => {
96+
mockConfig.assets_bundler = 'rspack';
97+
jest.doMock('shakapacker', () => ({ config: mockConfig }));
98+
const utils = require('../../../config/webpack/bundlerUtils');
99+
100+
const plugin = utils.getCssExtractPlugin();
101+
102+
expect(plugin).toBeDefined();
103+
// Rspack plugin class name
104+
expect(plugin.name).toBe('MockCssExtractRspackPlugin');
105+
});
106+
});
107+
});

config/swc.config.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ const customConfig = {
1010
loose: false,
1111
transform: {
1212
react: {
13-
// Use classic runtime for better SSR compatibility with React on Rails
13+
// Use classic runtime for SSR compatibility with React on Rails
14+
// This ensures React is explicitly imported in each component file, which
15+
// provides better compatibility with server-side rendering in Rails.
16+
// Note: React 19 supports automatic runtime with SSR, but classic runtime
17+
// is more explicit and avoids potential issues with different React versions.
1418
runtime: 'classic',
1519
// Enable React Fast Refresh in development
1620
refresh: env.isDevelopment && env.runningWebpackDevServer,

config/webpack/serverWebpackConfig.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,19 @@ const configureServer = () => {
3737
};
3838

3939
if (!serverEntry['server-bundle']) {
40+
const sourcePath = config.source_path || 'client/app';
41+
const entryPath = config.source_entry_path || 'packs';
42+
const fullPath = `${sourcePath}/${entryPath}/server-bundle.js`;
43+
4044
throw new Error(
41-
"Server bundle entry 'server-bundle' not found. " +
42-
"Check that client/app/packs/server-bundle.js exists and is configured in shakapacker.yml. " +
43-
"Verify nested_entries is set correctly and the file is in the source_entry_path.",
45+
`Server bundle entry 'server-bundle' not found.\n` +
46+
`Expected file: ${fullPath}\n` +
47+
`Current source_path: ${config.source_path}\n` +
48+
`Current source_entry_path: ${config.source_entry_path}\n` +
49+
`Verify:\n` +
50+
`1. The server-bundle.js file exists at the expected location\n` +
51+
`2. nested_entries is configured correctly in shakapacker.yml\n` +
52+
`3. The file is properly exported from your entry point`,
4453
);
4554
}
4655

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require 'yaml'
5+
6+
RSpec.describe 'Bundler Switching', type: :feature do
7+
let(:shakapacker_config_path) { Rails.root.join('config', 'shakapacker.yml') }
8+
let(:original_config) { YAML.load_file(shakapacker_config_path) }
9+
10+
after do
11+
# Restore original config after each test
12+
File.write(shakapacker_config_path, YAML.dump(original_config))
13+
end
14+
15+
describe 'switching between webpack and rspack' do
16+
it 'successfully builds with webpack' do
17+
# Set bundler to webpack
18+
config = original_config
19+
config['default']['assets_bundler'] = 'webpack'
20+
File.write(shakapacker_config_path, YAML.dump(config))
21+
22+
# Run build
23+
output = `NODE_ENV=test RAILS_ENV=test bin/shakapacker 2>&1`
24+
25+
expect($CHILD_STATUS.success?).to be true
26+
expect(output).to include('webpack')
27+
expect(output).to include('compiled successfully')
28+
end
29+
30+
it 'successfully builds with rspack' do
31+
# Set bundler to rspack
32+
config = original_config
33+
config['default']['assets_bundler'] = 'rspack'
34+
File.write(shakapacker_config_path, YAML.dump(config))
35+
36+
# Run build
37+
output = `NODE_ENV=test RAILS_ENV=test bin/shakapacker 2>&1`
38+
39+
expect($CHILD_STATUS.success?).to be true
40+
expect(output).to include('Rspack')
41+
expect(output).to include('compiled successfully')
42+
end
43+
44+
it 'produces functional bundles with both bundlers' do
45+
bundlers = %w[webpack rspack]
46+
47+
bundlers.each do |bundler|
48+
# Set bundler
49+
config = original_config
50+
config['default']['assets_bundler'] = bundler
51+
File.write(shakapacker_config_path, YAML.dump(config))
52+
53+
# Build
54+
`NODE_ENV=test RAILS_ENV=test bin/shakapacker 2>&1`
55+
expect($CHILD_STATUS.success?).to be true
56+
57+
# Verify manifest exists
58+
manifest_path = Rails.root.join('public', 'packs-test', 'manifest.json')
59+
expect(File.exist?(manifest_path)).to be true
60+
61+
# Verify server bundle exists
62+
server_bundle_path = Rails.root.join('ssr-generated', 'server-bundle.js')
63+
expect(File.exist?(server_bundle_path)).to be true
64+
end
65+
end
66+
end
67+
end

0 commit comments

Comments
 (0)