Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Changes since the last non-beta release.

### Added

- **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.
- **`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.
- **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.

Expand Down
46 changes: 40 additions & 6 deletions docs/css-modules-export-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,45 @@ If you prefer to keep the v8 default export behavior during migration, you can o

## Reverting to Default Exports (v8 Behavior)

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

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

This approach modifies the common webpack configuration that applies to all environments:
The simplest way to restore v8 behavior is to set the `css_modules_export_mode` option in your `config/shakapacker.yml`:

```yaml
# config/shakapacker.yml
default: &default
# ... other settings ...

# 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: default
```

This configuration automatically adjusts the CSS loader settings:

- Sets `namedExport: false` to enable default exports
- Sets `exportLocalsConvention: 'camelCase'` to export both original and camelCase versions

**Restart your development server** after changing this setting for the changes to take effect.

With this configuration, you can continue using v8-style imports:

```js
// Works with css_modules_export_mode: default
import styles from "./Component.module.css"
;<div className={styles.container}>
<button className={styles.button}>Click me</button>
<button className={styles["my-button"]}>Kebab-case</button>
<button className={styles.myButton}>Also available</button>
</div>
```

### Option 2: Manual Webpack Configuration (Advanced)

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:

```js
// config/webpack/commonWebpackConfig.js
Expand Down Expand Up @@ -198,7 +232,7 @@ const commonWebpackConfig = () => {
module.exports = commonWebpackConfig
```

### Option 2: Create `config/webpack/environment.js` (Alternative)
### Option 3: Create `config/webpack/environment.js` (Alternative)

If you prefer using a separate environment file:

Expand Down Expand Up @@ -239,12 +273,12 @@ module.exports = environment

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

### Option 3: (Optional) Sass Modules
### Option 4: (Optional) Sass Modules

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

```js
// For Option 1 approach, extend the overrideCssModulesConfig function:
// For Option 2 approach (manual webpack config), extend the overrideCssModulesConfig function:
const overrideCssModulesConfig = (config) => {
// Handle both CSS and SCSS rules
const styleRules = config.module.rules.filter(
Expand Down
13 changes: 11 additions & 2 deletions docs/v9_upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,18 @@ import * as styles from './Component.module.css';
- TypeScript: Change to namespace imports (`import * as styles`)
- Kebab-case class names are automatically converted to camelCase

2. **Keep v8 behavior** temporarily:
2. **Keep v8 behavior** temporarily using configuration file (Easiest):

```yaml
# config/shakapacker.yml
css_modules_export_mode: default
```

This allows you to keep using default imports while migrating gradually

3. **Keep v8 behavior** using webpack configuration (Advanced):
- Override the css-loader configuration as shown in [CSS Modules Export Mode documentation](./css-modules-export-mode.md)
- This gives you time to migrate gradually
- Provides more control over the configuration

**Benefits of the change:**

Expand Down
12 changes: 12 additions & 0 deletions lib/install/config/shakapacker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ default: &default
# css_extract_ignore_order_warnings to true
css_extract_ignore_order_warnings: false

# CSS Modules export mode
# Controls how CSS Module class names are exported in JavaScript
# Defaults to 'named' if not specified. Uncomment and change to 'default' for v8 behavior.
# Options:
# - named (default): Use named exports with camelCase conversion (v9 default)
# Example: import { button } from './styles.module.css'
# - default: Use default export with both original and camelCase names (v8 behavior)
# Example: import styles from './styles.module.css'
# For gradual migration, you can set this to default to maintain v8 behavior
# See https://github.com/shakacode/shakapacker/blob/main/docs/css-modules-export-mode.md
# css_modules_export_mode: named

public_root_path: public
public_output_path: packs
cache_path: tmp/shakapacker
Expand Down
25 changes: 25 additions & 0 deletions lib/shakapacker/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,31 @@ def webpack_loader
javascript_transpiler
end

# Returns the CSS Modules export mode configuration
#
# Controls how CSS Module class names are exported in JavaScript:
# - "named" (default): Use named exports with camelCase conversion (v9 behavior)
# - "default": Use default export with both original and camelCase names (v8 behavior)
#
# @return [String] "named" or "default"
# @raise [ArgumentError] if an invalid value is configured
def css_modules_export_mode
@css_modules_export_mode ||= begin
mode = fetch(:css_modules_export_mode) || "named"

# Validate the configuration value
valid_modes = ["named", "default"]
unless valid_modes.include?(mode)
raise ArgumentError,
"Invalid css_modules_export_mode: '#{mode}'. " \
"Valid values are: #{valid_modes.map { |m| "'#{m}'" }.join(', ')}. " \
"See https://github.com/shakacode/shakapacker/blob/main/docs/css-modules-export-mode.md"
end

mode
end
end

# Returns the path to the bundler configuration directory
#
# This is where webpack.config.js or rspack.config.js should be located.
Expand Down
1 change: 1 addition & 0 deletions package/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface Config {
source_entry_path: string
nested_entries: boolean
css_extract_ignore_order_warnings: boolean
css_modules_export_mode?: "named" | "default"
public_root_path: string
public_output_path: string
private_output_path?: string
Expand Down
17 changes: 12 additions & 5 deletions package/utils/getStyleRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ const getStyleRule = (
? requireOrError("@rspack/core").CssExtractRspackPlugin.loader
: requireOrError("mini-css-extract-plugin").loader

// Determine CSS Modules export mode based on configuration
// 'named' (default): Use named exports with camelCaseOnly (v9 behavior)
// 'default': Use default exports with camelCase (v8 behavior)
const useNamedExports = config.css_modules_export_mode !== "default"

const use = [
inliningCss ? "style-loader" : extractionPlugin,
{
Expand All @@ -38,11 +43,13 @@ const getStyleRule = (
importLoaders: 2,
modules: {
auto: true,
// v9 defaults: Use named exports with camelCase conversion
// Note: css-loader requires 'camelCaseOnly' or 'dashesOnly' when namedExport is true
// Using 'camelCase' with namedExport: true causes a build error
namedExport: true,
exportLocalsConvention: "camelCaseOnly"
// Use named exports for v9 (default), or default exports for v8 compatibility
namedExport: useNamedExports,
// 'camelCaseOnly' with namedExport: true (v9 default)
// 'camelCase' with namedExport: false (v8 behavior - exports both original and camelCase)
exportLocalsConvention: useNamedExports
? "camelCaseOnly"
: "camelCase"
}
}
},
Expand Down
56 changes: 48 additions & 8 deletions spec/shakapacker/css_modules_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -181,17 +181,57 @@
# Read the TypeScript source to verify configuration
style_rule_content = File.read("package/utils/getStyleRule.ts")

# Should have namedExport: true
expect(style_rule_content).to include("namedExport: true")
# Should have conditional logic based on css_modules_export_mode
expect(style_rule_content).to include("css_modules_export_mode")
expect(style_rule_content).to include("useNamedExports")

# Should have exportLocalsConvention: 'camelCaseOnly' (not 'camelCase')
expect(style_rule_content).to include('exportLocalsConvention: "camelCaseOnly"')
# Should set namedExport conditionally
expect(style_rule_content).to include("namedExport: useNamedExports")

# Should NOT have the invalid 'camelCase' with namedExport: true
expect(style_rule_content).not_to include('exportLocalsConvention: "camelCase"')
# Should set exportLocalsConvention conditionally
expect(style_rule_content).to include("exportLocalsConvention: useNamedExports")
expect(style_rule_content).to include('"camelCaseOnly"')
expect(style_rule_content).to include('"camelCase"')

# Should have explanatory comment about the requirement
expect(style_rule_content).to include("css-loader requires 'camelCaseOnly' or 'dashesOnly'")
# Should have explanatory comments about v9 and v8 behavior
expect(style_rule_content).to include("v9 behavior")
expect(style_rule_content).to include("v8 behavior")
end

describe "css_modules_export_mode configuration" do
it "defaults to 'named' when not specified" do
# The test config doesn't have css_modules_export_mode set
expect(config.css_modules_export_mode).to eq("named")
end

it "accepts 'default' as a valid value" do
allow(config).to receive(:fetch).with(:css_modules_export_mode).and_return("default")
expect(config.css_modules_export_mode).to eq("default")
end

it "raises ArgumentError for invalid values" do
allow(config).to receive(:fetch).with(:css_modules_export_mode).and_return("invalid")

expect {
config.css_modules_export_mode
}.to raise_error(ArgumentError, /Invalid css_modules_export_mode: 'invalid'/)
end

it "provides helpful error message with valid options" do
allow(config).to receive(:fetch).with(:css_modules_export_mode).and_return("foobar")

expect {
config.css_modules_export_mode
}.to raise_error(ArgumentError, /Valid values are: 'named', 'default'/)
end

it "includes documentation link in error message" do
allow(config).to receive(:fetch).with(:css_modules_export_mode).and_return("bad")

expect {
config.css_modules_export_mode
}.to raise_error(ArgumentError, /css-modules-export-mode\.md/)
end
end
end

Expand Down