Skip to content

Commit 272cb11

Browse files
justin808claude
andauthored
Add css_modules_export_mode configuration option (#817)
## Summary Introduces a simple, user-friendly configuration option to control CSS Modules export mode, making it much easier to restore v8 behavior without manual webpack configuration overrides. ## Problem Solved Issue #809 identified that upgrading from Shakapacker v8 to v9 requires complex webpack configuration overrides to maintain v8 CSS Modules behavior. This creates a high barrier for teams with large codebases who need to migrate gradually. ## Solution Add a `css_modules_export_mode` configuration option to `shakapacker.yml`: ```yaml # config/shakapacker.yml default: &default # CSS Modules export mode # 'named' (default) - Use named exports with camelCase conversion (v9 default) # 'default' - Use default export with both original and camelCase names (v8 behavior) css_modules_export_mode: "named" ``` ## Changes 1. **Configuration Schema** (`lib/install/config/shakapacker.yml`) - Added `css_modules_export_mode` option with clear documentation - Defaults to `"named"` to maintain v9 behavior 2. **TypeScript Types** (`package/types.ts`) - Added `css_modules_export_mode?: "named" | "default"` to Config interface - Provides type safety and IDE autocomplete 3. **Implementation** (`package/utils/getStyleRule.ts`) - Reads configuration and applies appropriate CSS loader settings - `"named"`: Sets `namedExport: true` and `exportLocalsConvention: "camelCaseOnly"` (v9 default) - `"default"`: Sets `namedExport: false` and `exportLocalsConvention: "camelCase"` (v8 behavior) 4. **Documentation Updates** - Updated `docs/css-modules-export-mode.md` with new configuration approach - Updated `docs/v9_upgrade.md` to highlight the simple config option first - Reorganized migration options to emphasize the easiest approach ## Benefits - **No webpack expertise required** - Users just change one line in YAML - **Reduced migration friction** - Large codebases can upgrade immediately - **Clear migration path** - Simple to enable v8 mode, then gradually migrate - **Maintains best practices** - Defaults to v9 behavior, but provides escape hatch - **Better than PR #810** - While that PR improved documentation, this provides a programmatic solution ## Example Usage ### Keeping v8 Behavior ```yaml # config/shakapacker.yml css_modules_export_mode: "default" ``` Then continue using v8-style imports: ```javascript import styles from './Component.module.css'; <button className={styles.button} /> ``` ### Using v9 Behavior (Default) ```yaml # config/shakapacker.yml css_modules_export_mode: "named" # or omit this line ``` Use v9-style imports: ```javascript // JavaScript import { button } from './Component.module.css'; <button className={button} /> // TypeScript import * as styles from './Component.module.css'; <button className={styles.button} /> ``` ## Migration Strategy 1. **Immediate upgrade**: Set `css_modules_export_mode: "default"` to maintain v8 behavior 2. **Gradual migration**: Update components incrementally to use named exports 3. **Complete migration**: Remove the config option to use v9 default ## Test Plan - [x] Linting passes (`yarn lint`) - [x] RuboCop passes (`bundle exec rubocop`) - [x] TypeScript type checking passes (`yarn run tsc --noEmit`) - [x] Configuration is properly typed - [x] Documentation is clear and comprehensive ## Related - Addresses issue #809 - Complements PR #810 (documentation improvements) - Supersedes manual webpack configuration approach Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a configurable CSS Modules export mode allowing projects to use either v8-style default imports or v9-style named exports. * **Documentation** * Expanded upgrade and CSS Modules guides with step-by-step configuration examples and migration options (easy config-driven or advanced manual approaches). * **Tests** * Updated test coverage to validate mode-dependent CSS Modules behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude <[email protected]>
1 parent 10d2164 commit 272cb11

File tree

8 files changed

+150
-21
lines changed

8 files changed

+150
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Changes since the last non-beta release.
1717

1818
### Added
1919

20+
- **Support for `css_modules_export_mode` configuration option**. [PR #817](https://github.com/shakacode/shakapacker/pull/817) by [justin808](https://github.com/justin808). Adds `css_modules_export_mode` setting in `shakapacker.yml` to control CSS Modules export style. Set to `"named"` (default, v9+ behavior with true named exports) or `"default"` (v8 behavior with default export object). Allows teams to opt into v8-style exports for easier migration from v8 or when using TypeScript with strict type checking.
2021
- **`Configuration#data` public API method** with enhanced documentation and safety. [PR #820](https://github.com/shakacode/shakapacker/pull/820) by [justin808](https://github.com/justin808). The `Configuration#data` method is now part of the public Ruby API, providing stable access to raw configuration data. Returns a frozen hash with symbolized keys to prevent accidental mutations. Includes comprehensive test coverage and detailed RDoc documentation.
2122
- **Support for `javascript_transpiler: 'none'`** for completely custom webpack configurations. [PR #799](https://github.com/shakacode/shakapacker/pull/799) by [justin808](https://github.com/justin808). Allows users with custom webpack configs to skip Shakapacker's transpiler setup and validation by setting `javascript_transpiler: 'none'` in `shakapacker.yml`. Useful when managing transpilation entirely outside of Shakapacker's defaults.
2223

docs/css-modules-export-mode.md

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,45 @@ If you prefer to keep the v8 default export behavior during migration, you can o
150150

151151
## Reverting to Default Exports (v8 Behavior)
152152

153-
To use the v8-style default exports instead of v9's named exports:
153+
To use the v8-style default exports instead of v9's named exports, you have several options:
154154

155-
### Option 1: Update `config/webpack/commonWebpackConfig.js` (Recommended)
155+
### Option 1: Configuration File (Easiest - Recommended)
156156

157-
This approach modifies the common webpack configuration that applies to all environments:
157+
The simplest way to restore v8 behavior is to set the `css_modules_export_mode` option in your `config/shakapacker.yml`:
158+
159+
```yaml
160+
# config/shakapacker.yml
161+
default: &default
162+
# ... other settings ...
163+
164+
# CSS Modules export mode
165+
# named (default) - Use named exports with camelCase conversion (v9 default)
166+
# default - Use default export with both original and camelCase names (v8 behavior)
167+
css_modules_export_mode: default
168+
```
169+
170+
This configuration automatically adjusts the CSS loader settings:
171+
172+
- Sets `namedExport: false` to enable default exports
173+
- Sets `exportLocalsConvention: 'camelCase'` to export both original and camelCase versions
174+
175+
**Restart your development server** after changing this setting for the changes to take effect.
176+
177+
With this configuration, you can continue using v8-style imports:
178+
179+
```js
180+
// Works with css_modules_export_mode: default
181+
import styles from "./Component.module.css"
182+
;<div className={styles.container}>
183+
<button className={styles.button}>Click me</button>
184+
<button className={styles["my-button"]}>Kebab-case</button>
185+
<button className={styles.myButton}>Also available</button>
186+
</div>
187+
```
188+
189+
### Option 2: Manual Webpack Configuration (Advanced)
190+
191+
If you need more control or can't use the configuration file approach, you can manually modify the webpack configuration that applies to all environments:
158192

159193
```js
160194
// config/webpack/commonWebpackConfig.js
@@ -198,7 +232,7 @@ const commonWebpackConfig = () => {
198232
module.exports = commonWebpackConfig
199233
```
200234

201-
### Option 2: Create `config/webpack/environment.js` (Alternative)
235+
### Option 3: Create `config/webpack/environment.js` (Alternative)
202236

203237
If you prefer using a separate environment file:
204238

@@ -239,12 +273,12 @@ module.exports = environment
239273

240274
Then reference this in your environment-specific configs (development.js, production.js, etc.).
241275

242-
### Option 3: (Optional) Sass Modules
276+
### Option 4: (Optional) Sass Modules
243277

244278
If you also use Sass modules, add similar configuration for SCSS files:
245279

246280
```js
247-
// For Option 1 approach, extend the overrideCssModulesConfig function:
281+
// For Option 2 approach (manual webpack config), extend the overrideCssModulesConfig function:
248282
const overrideCssModulesConfig = (config) => {
249283
// Handle both CSS and SCSS rules
250284
const styleRules = config.module.rules.filter(

docs/v9_upgrade.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,18 @@ import * as styles from './Component.module.css';
145145
- TypeScript: Change to namespace imports (`import * as styles`)
146146
- Kebab-case class names are automatically converted to camelCase
147147

148-
2. **Keep v8 behavior** temporarily:
148+
2. **Keep v8 behavior** temporarily using configuration file (Easiest):
149+
150+
```yaml
151+
# config/shakapacker.yml
152+
css_modules_export_mode: default
153+
```
154+
155+
This allows you to keep using default imports while migrating gradually
156+
157+
3. **Keep v8 behavior** using webpack configuration (Advanced):
149158
- Override the css-loader configuration as shown in [CSS Modules Export Mode documentation](./css-modules-export-mode.md)
150-
- This gives you time to migrate gradually
159+
- Provides more control over the configuration
151160
152161
**Benefits of the change:**
153162

lib/install/config/shakapacker.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ default: &default
1919
# css_extract_ignore_order_warnings to true
2020
css_extract_ignore_order_warnings: false
2121

22+
# CSS Modules export mode
23+
# Controls how CSS Module class names are exported in JavaScript
24+
# Defaults to 'named' if not specified. Uncomment and change to 'default' for v8 behavior.
25+
# Options:
26+
# - named (default): Use named exports with camelCase conversion (v9 default)
27+
# Example: import { button } from './styles.module.css'
28+
# - default: Use default export with both original and camelCase names (v8 behavior)
29+
# Example: import styles from './styles.module.css'
30+
# For gradual migration, you can set this to default to maintain v8 behavior
31+
# See https://github.com/shakacode/shakapacker/blob/main/docs/css-modules-export-mode.md
32+
# css_modules_export_mode: named
33+
2234
public_root_path: public
2335
public_output_path: packs
2436
cache_path: tmp/shakapacker

lib/shakapacker/configuration.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,31 @@ def webpack_loader
339339
javascript_transpiler
340340
end
341341

342+
# Returns the CSS Modules export mode configuration
343+
#
344+
# Controls how CSS Module class names are exported in JavaScript:
345+
# - "named" (default): Use named exports with camelCase conversion (v9 behavior)
346+
# - "default": Use default export with both original and camelCase names (v8 behavior)
347+
#
348+
# @return [String] "named" or "default"
349+
# @raise [ArgumentError] if an invalid value is configured
350+
def css_modules_export_mode
351+
@css_modules_export_mode ||= begin
352+
mode = fetch(:css_modules_export_mode) || "named"
353+
354+
# Validate the configuration value
355+
valid_modes = ["named", "default"]
356+
unless valid_modes.include?(mode)
357+
raise ArgumentError,
358+
"Invalid css_modules_export_mode: '#{mode}'. " \
359+
"Valid values are: #{valid_modes.map { |m| "'#{m}'" }.join(', ')}. " \
360+
"See https://github.com/shakacode/shakapacker/blob/main/docs/css-modules-export-mode.md"
361+
end
362+
363+
mode
364+
end
365+
end
366+
342367
# Returns the path to the bundler configuration directory
343368
#
344369
# This is where webpack.config.js or rspack.config.js should be located.

package/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface Config {
1515
source_entry_path: string
1616
nested_entries: boolean
1717
css_extract_ignore_order_warnings: boolean
18+
css_modules_export_mode?: "named" | "default"
1819
public_root_path: string
1920
public_output_path: string
2021
private_output_path?: string

package/utils/getStyleRule.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ const getStyleRule = (
2929
? requireOrError("@rspack/core").CssExtractRspackPlugin.loader
3030
: requireOrError("mini-css-extract-plugin").loader
3131

32+
// Determine CSS Modules export mode based on configuration
33+
// 'named' (default): Use named exports with camelCaseOnly (v9 behavior)
34+
// 'default': Use default exports with camelCase (v8 behavior)
35+
const useNamedExports = config.css_modules_export_mode !== "default"
36+
3237
const use = [
3338
inliningCss ? "style-loader" : extractionPlugin,
3439
{
@@ -38,11 +43,13 @@ const getStyleRule = (
3843
importLoaders: 2,
3944
modules: {
4045
auto: true,
41-
// v9 defaults: Use named exports with camelCase conversion
42-
// Note: css-loader requires 'camelCaseOnly' or 'dashesOnly' when namedExport is true
43-
// Using 'camelCase' with namedExport: true causes a build error
44-
namedExport: true,
45-
exportLocalsConvention: "camelCaseOnly"
46+
// Use named exports for v9 (default), or default exports for v8 compatibility
47+
namedExport: useNamedExports,
48+
// 'camelCaseOnly' with namedExport: true (v9 default)
49+
// 'camelCase' with namedExport: false (v8 behavior - exports both original and camelCase)
50+
exportLocalsConvention: useNamedExports
51+
? "camelCaseOnly"
52+
: "camelCase"
4653
}
4754
}
4855
},

spec/shakapacker/css_modules_spec.rb

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -181,17 +181,57 @@
181181
# Read the TypeScript source to verify configuration
182182
style_rule_content = File.read("package/utils/getStyleRule.ts")
183183

184-
# Should have namedExport: true
185-
expect(style_rule_content).to include("namedExport: true")
184+
# Should have conditional logic based on css_modules_export_mode
185+
expect(style_rule_content).to include("css_modules_export_mode")
186+
expect(style_rule_content).to include("useNamedExports")
186187

187-
# Should have exportLocalsConvention: 'camelCaseOnly' (not 'camelCase')
188-
expect(style_rule_content).to include('exportLocalsConvention: "camelCaseOnly"')
188+
# Should set namedExport conditionally
189+
expect(style_rule_content).to include("namedExport: useNamedExports")
189190

190-
# Should NOT have the invalid 'camelCase' with namedExport: true
191-
expect(style_rule_content).not_to include('exportLocalsConvention: "camelCase"')
191+
# Should set exportLocalsConvention conditionally
192+
expect(style_rule_content).to include("exportLocalsConvention: useNamedExports")
193+
expect(style_rule_content).to include('"camelCaseOnly"')
194+
expect(style_rule_content).to include('"camelCase"')
192195

193-
# Should have explanatory comment about the requirement
194-
expect(style_rule_content).to include("css-loader requires 'camelCaseOnly' or 'dashesOnly'")
196+
# Should have explanatory comments about v9 and v8 behavior
197+
expect(style_rule_content).to include("v9 behavior")
198+
expect(style_rule_content).to include("v8 behavior")
199+
end
200+
201+
describe "css_modules_export_mode configuration" do
202+
it "defaults to 'named' when not specified" do
203+
# The test config doesn't have css_modules_export_mode set
204+
expect(config.css_modules_export_mode).to eq("named")
205+
end
206+
207+
it "accepts 'default' as a valid value" do
208+
allow(config).to receive(:fetch).with(:css_modules_export_mode).and_return("default")
209+
expect(config.css_modules_export_mode).to eq("default")
210+
end
211+
212+
it "raises ArgumentError for invalid values" do
213+
allow(config).to receive(:fetch).with(:css_modules_export_mode).and_return("invalid")
214+
215+
expect {
216+
config.css_modules_export_mode
217+
}.to raise_error(ArgumentError, /Invalid css_modules_export_mode: 'invalid'/)
218+
end
219+
220+
it "provides helpful error message with valid options" do
221+
allow(config).to receive(:fetch).with(:css_modules_export_mode).and_return("foobar")
222+
223+
expect {
224+
config.css_modules_export_mode
225+
}.to raise_error(ArgumentError, /Valid values are: 'named', 'default'/)
226+
end
227+
228+
it "includes documentation link in error message" do
229+
allow(config).to receive(:fetch).with(:css_modules_export_mode).and_return("bad")
230+
231+
expect {
232+
config.css_modules_export_mode
233+
}.to raise_error(ArgumentError, /css-modules-export-mode\.md/)
234+
end
195235
end
196236
end
197237

0 commit comments

Comments
 (0)