Skip to content
Merged
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
288 changes: 288 additions & 0 deletions docs/css-modules-export-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
# CSS Modules Export Mode

Most React guides and tutorials expect to import CSS Modules using a **default export object**:

```js
import styles from './Foo.module.css';
<button className={styles.bright} />
```

However, depending on configuration, `css-loader` may instead emit **named exports**:

```js
import { bright } from './Foo.module.css';
<button className={bright} />
```

By default, Shakapacker currently leaves `css-loader`'s `modules.namedExport` option unset, which leads to **named exports** being used in many cases. This can surprise developers expecting the `import styles ...` pattern.

---

## How to Configure Shakapacker for Default Exports

To force the more familiar `import styles ...` behavior (i.e. `namedExport: false`), update your webpack configuration as follows.

### Option 1: Update `config/webpack/commonWebpackConfig.js` (Recommended)

This approach modifies the common webpack configuration that applies to all environments:

```js
// config/webpack/commonWebpackConfig.js
const { generateWebpackConfig, merge } = require('shakapacker');

const baseClientWebpackConfig = generateWebpackConfig();

// Override CSS Modules configuration to use default exports instead of named exports
const overrideCssModulesConfig = (config) => {
// Find the CSS rule in the module rules
const cssRule = config.module.rules.find(rule =>
rule.test && rule.test.toString().includes('css')
);

if (cssRule && cssRule.use) {
const cssLoaderUse = cssRule.use.find(use =>
use.loader && use.loader.includes('css-loader')
);

if (cssLoaderUse && cssLoaderUse.options && cssLoaderUse.options.modules) {
// Set namedExport to false for default export behavior
cssLoaderUse.options.modules.namedExport = false;
cssLoaderUse.options.modules.exportLocalsConvention = 'asIs';
}
}

return config;
};

const commonOptions = {
resolve: {
extensions: ['.css', '.ts', '.tsx'],
},
};

const commonWebpackConfig = () => {
const config = merge({}, baseClientWebpackConfig, commonOptions);
return overrideCssModulesConfig(config);
};

module.exports = commonWebpackConfig;
```

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

If you prefer using a separate environment file:

```js
// config/webpack/environment.js
const { environment } = require('@shakacode/shakapacker');
const getStyleRule = require('@shakacode/shakapacker/package/utils/getStyleRule');

// CSS Modules rule for *.module.css with default export enabled
const cssModulesRule = getStyleRule(/\.module\.css$/i, [], {
sourceMap: true,
importLoaders: 2,
modules: {
auto: true,
namedExport: false, // <-- key: enable default export object
exportLocalsConvention: 'asIs' // keep your class names as-is
}
});

// Ensure this rule wins for *.module.css
if (cssModulesRule) {
environment.loaders.prepend('css-modules', cssModulesRule);
}

// Plain CSS rule for non-modules
const plainCssRule = getStyleRule(/(?<!\.module)\.css$/i, [], {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Verify the negative lookahead regex pattern for cross-platform compatibility.

The negative lookbehind (?<!\.module) may not be supported in all JavaScript engines or webpack versions. Consider using a more compatible approach.

If compatibility issues arise, consider this alternative approach:

-// Plain CSS rule for non-modules
-const plainCssRule = getStyleRule(/(?<!\.module)\.css$/i, [], {
+// Plain CSS rule for non-modules (more compatible pattern)
+const plainCssRule = getStyleRule(/\.css$/i, [], {
   sourceMap: true,
   importLoaders: 2,
-  modules: false
+  modules: {
+    auto: (resourcePath) => !resourcePath.includes('.module.css')
+  }
 });

🏁 Script executed:

#!/bin/bash
# Description: Check if the negative lookbehind regex pattern is widely supported
# in the versions of Node.js and webpack typically used with Shakapacker

echo "Checking Node.js version support for negative lookbehind patterns..."
node -e "
try {
  const regex = /(?<!\.module)\.css$/i;
  console.log('✓ Negative lookbehind supported in Node.js', process.version);
  console.log('Test: \"style.css\" matches:', regex.test('style.css'));
  console.log('Test: \"style.module.css\" matches:', regex.test('style.module.css'));
} catch (e) {
  console.error('✗ Negative lookbehind NOT supported:', e.message);
}
"

echo -e "\nChecking webpack documentation for css-loader regex patterns..."

Length of output: 556


Replace unsupported negative lookbehind with a compatible pattern and use css-loader modules.auto

Sandbox Node v24.3.0 throws a SyntaxError for /(?<!.module).css$/i — negative lookbehind is not safe; update the docs/example at docs/css-modules-export-mode.md:97.

-// Plain CSS rule for non-modules
-const plainCssRule = getStyleRule(/(?<!\.module)\.css$/i, [], {
+// Plain CSS rule for non-modules (more compatible pattern)
+const plainCssRule = getStyleRule(/\.css$/i, [], {
   sourceMap: true,
   importLoaders: 2,
-  modules: false
+  modules: {
+    auto: (resourcePath) => !resourcePath.includes('.module.css')
+  }
 });

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In docs/css-modules-export-mode.md around line 97, replace the unsupported
negative lookbehind regex /(?<!\.module)\.css$/i with a compatible approach: use
a plain /\.css$/i matcher (or keep /\.css$/i and explicitly exclude
/\.module\.css$/i) and configure css-loader to use modules.auto (e.g. modules: {
auto: resourcePath => /\.module\.css$/i.test(resourcePath) }) so module CSS is
detected without relying on lookbehind; update the example and any comments to
show the new regex and the modules.auto usage.

sourceMap: true,
importLoaders: 2,
modules: false
});

if (plainCssRule) {
environment.loaders.append('css', plainCssRule);
}

module.exports = environment;
```

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

### Option 3: (Optional) Sass Modules

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

```js
// For Option 1 approach, extend the overrideCssModulesConfig function:
const overrideCssModulesConfig = (config) => {
// Handle both CSS and SCSS rules
const styleRules = config.module.rules.filter(rule =>
rule.test && (rule.test.toString().includes('css') || rule.test.toString().includes('scss'))
);

styleRules.forEach(rule => {
if (rule.use) {
const cssLoaderUse = rule.use.find(use =>
use.loader && use.loader.includes('css-loader')
);

if (cssLoaderUse && cssLoaderUse.options && cssLoaderUse.options.modules) {
cssLoaderUse.options.modules.namedExport = false;
cssLoaderUse.options.modules.exportLocalsConvention = 'asIs';
}
}
});

return config;
};
```

---

## Verifying the Configuration

### 1. Rebuild Your Packs

After making the configuration changes, rebuild your webpack bundles:

```bash
# For development
NODE_ENV=development bin/shakapacker

# Or with the dev server
bin/shakapacker-dev-server
```

### 2. Test in Your React Component

Update your component to use default imports:

```js
// Before (named exports)
import { bright } from './Foo.module.css';

// After (default export)
import styles from './Foo.module.css';
console.log(styles); // { bright: 'Foo_bright__hash' }
```

### 3. Debug Webpack Configuration (Optional)

To inspect the final webpack configuration:

```bash
NODE_ENV=development bin/shakapacker --profile --json > /tmp/webpack-stats.json
```

Then search for `css-loader` options in the generated JSON file.

---

## Benefits of Default Export Approach

1. **Better Developer Experience**: Matches most React tutorials and documentation
2. **IDE Support**: Better autocomplete and IntelliSense for CSS class names
3. **Type Safety**: Easier to add TypeScript definitions for CSS modules
4. **Consistency**: Aligns with common React ecosystem practices

---

## Migration Guide

If you're migrating from named exports to default exports:

### 1. Update Import Statements

```js
// Old (named exports)
import { bright, container, button } from './Component.module.css';

// New (default export)
import styles from './Component.module.css';
```

### 2. Update Class References

```js
// Old
<div className={container}>
<button className={button}>Click me</button>
<span className={bright}>Highlighted text</span>
</div>

// New
<div className={styles.container}>
<button className={styles.button}>Click me</button>
<span className={styles.bright}>Highlighted text</span>
</div>
```

### 3. Consider Using a Codemod

For large codebases, consider writing a codemod to automate the migration:

```bash
# Example using jscodeshift (pseudocode)
npx jscodeshift -t css-modules-migration.js src/
```

---

## Future Shakapacker Configuration

In future versions of Shakapacker, this configuration may be exposed via `config/shakapacker.yml`:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

TIL that this is actually defaulted based on esModule option https://webpack.js.org/loaders/css-loader/#namedexport

Agree that using false makes sense down the line though that is a breaking change warranting major release 👍


```yml
# Future configuration (not yet implemented)
css_modules:
# true -> named exports (import { bright } ...)
# false -> default export (import styles ...)
named_export: false
```

- **Current behavior:** Uses named exports when unset
- **Future behavior:** New app templates will default to `false`
- **Next major release:** The default will change to `false` when unset

---

## Troubleshooting

### CSS Classes Not Applying

If your CSS classes aren't applying after the change:

1. **Check import syntax**: Ensure you're using `import styles from ...`
2. **Verify class names**: Use `console.log(styles)` to see available classes
3. **Rebuild webpack**: Clear cache and rebuild: `rm -rf tmp/cache && bin/shakapacker`

### TypeScript Support

For TypeScript projects, create type definitions for your CSS modules:

```typescript
// src/types/css-modules.d.ts
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}
```

### Build Performance

The configuration changes should not impact build performance significantly. If you experience issues:

1. Check webpack stats: `bin/shakapacker --profile`
2. Verify only necessary rules are being modified
3. Consider using webpack bundle analyzer for deeper insights

---

## Summary

- **Current default**: Named exports (`import { bright } ...`)
- **Recommended for DX**: Default export (`import styles ...`)
- **Implementation**: Override CSS loader configuration in `commonWebpackConfig.js`
- **Migration**: Update imports and class references systematically
- **Future**: Shakapacker will provide native configuration options
Loading