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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,11 @@ Changes since the last non-beta release.
```

2. **CSS Modules now use named exports by default**
- Configured with `namedExport: true` and `exportLocalsConvention: 'camelCase'`
- Configured with `namedExport: true` and `exportLocalsConvention: 'camelCaseOnly'`
- **JavaScript:** Use named imports: `import { className } from './styles.module.css'`
- **TypeScript:** Use namespace imports: `import * as styles from './styles.module.css'`
- Default imports (`import styles from '...'`) no longer work
- **Note:** css-loader requires `'camelCaseOnly'` or `'dashesOnly'` when `namedExport: true`. Using `'camelCase'` causes a build error.
- See [CSS Modules Export Mode documentation](./docs/css-modules-export-mode.md) for migration details

3. **Configuration option renamed from `webpack_loader` to `javascript_transpiler`**
Expand Down
7 changes: 5 additions & 2 deletions TODO_v9.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ Align with Next.js and modern tooling by using named exports:
options: {
modules: {
namedExport: true,
exportLocalsConvention: 'camelCase'
exportLocalsConvention: 'camelCaseOnly' // Must be 'camelCaseOnly' or 'dashesOnly' with namedExport: true
}
}
}
```

**Note:** Using `exportLocalsConvention: 'camelCase'` with `namedExport: true` will cause a build error.
css-loader only allows `'camelCaseOnly'` or `'dashesOnly'` when named exports are enabled.

2. **Update TypeScript types:**
- Ensure proper typing for CSS modules with named exports
- May need to update or generate `.d.ts` files for CSS modules
Expand Down Expand Up @@ -81,4 +84,4 @@ Align with Next.js and modern tooling by using named exports:
### Test Infrastructure
- Successfully implemented dual bundler support (webpack/rspack)
- test-bundler script working well with status command
- Consider adding more comprehensive tests for both bundlers
- Consider adding more comprehensive tests for both bundlers
110 changes: 102 additions & 8 deletions docs/css-modules-export-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,55 @@ import * as styles from './Foo.module.css';
- Aligns with modern JavaScript module standards
- Automatically converts kebab-case to camelCase (`my-button` → `myButton`)

### Important: exportLocalsConvention with namedExport

When `namedExport: true` is enabled (v9 default), css-loader requires `exportLocalsConvention` to be either `'camelCaseOnly'` or `'dashesOnly'`.

**The following will cause a build error:**
```js
modules: {
namedExport: true,
exportLocalsConvention: 'camelCase' // ❌ ERROR: incompatible with namedExport: true
}
```

**Error message:**
```
"exportLocalsConvention" with "camelCase" value is incompatible with "namedExport: true" option
```

**Correct v9 configuration:**
```js
modules: {
namedExport: true,
exportLocalsConvention: 'camelCaseOnly' // ✅ Correct - only camelCase exported
}
```

**exportLocalsConvention options with namedExport:**

When `namedExport: true`, you can use:
- `'camelCaseOnly'` (v9 default): Exports ONLY the camelCase version (e.g., only `myButton`)
- `'dashesOnly'`: Exports ONLY the original kebab-case version (e.g., only `my-button`)

**Not compatible with namedExport: true:**
- `'camelCase'`: Exports both versions (both `my-button` and `myButton`) - only works with `namedExport: false` (v8 behavior)

**Configuration Quick Reference:**

| namedExport | exportLocalsConvention | `.my-button` exports | Use Case | Compatible? |
|-------------|------------------------|---------------------|----------|-------------|
| `true` | `'camelCaseOnly'` | `myButton` | JavaScript conventions | ✅ Valid |
| `true` | `'dashesOnly'` | `'my-button'` | Preserve CSS naming | ✅ Valid |
| `false` | `'camelCase'` | Both `myButton` AND `'my-button'` | v8 compatibility | ✅ Valid |
| `false` | `'asIs'` | `'my-button'` | No transformation | ✅ Valid |
| `true` | `'camelCase'` | - | - | ❌ Build Error |

**When to use each option:**
- Use `'camelCaseOnly'` if you prefer standard JavaScript naming conventions
- Use `'dashesOnly'` if you want to preserve your CSS class names exactly as written
- Use `'camelCase'` (with `namedExport: false`) only if you need both versions available

## Version 8.x and Earlier Behavior

In Shakapacker v8 and earlier, the default behavior was to use a **default export object**:
Expand Down Expand Up @@ -244,7 +293,9 @@ import { bright, container, button } from './Component.module.css';

#### 3. Handle Kebab-Case Class Names

With v9's `exportLocalsConvention: 'camelCase'`, kebab-case class names are automatically converted:
**Option A: Use camelCase (v9 default)**

With `exportLocalsConvention: 'camelCaseOnly'`, kebab-case class names are automatically converted:

```css
/* styles.module.css */
Expand All @@ -253,13 +304,35 @@ With v9's `exportLocalsConvention: 'camelCase'`, kebab-case class names are auto
```

```js
// v9 imports (camelCase conversion)
// v9 default - camelCase conversion
import { myButton, primaryColor } from './styles.module.css';
<button className={myButton} />
```

// Use the camelCase versions in your components
**Option B: Keep kebab-case with 'dashesOnly'**

If you prefer to preserve the original kebab-case names, configure your webpack to use `'dashesOnly'`:

```js
// config/webpack/commonWebpackConfig.js
modules: {
namedExport: true,
exportLocalsConvention: 'dashesOnly'
}
```

```js
// With dashesOnly - preserve kebab-case
import * as styles from './styles.module.css';
<button className={styles['my-button']} />

// Or with aliasing:
import { 'my-button': myButton } from './styles.module.css';
<button className={myButton} />
```

**Note:** With both `'camelCaseOnly'` and `'dashesOnly'`, only one version of each class name is exported. The original kebab-case name is NOT available with `'camelCaseOnly'`, and the camelCase version is NOT available with `'dashesOnly'`.

#### 4. Using a Codemod for Large Codebases

For large codebases, you can create a codemod to automate the migration:
Expand Down Expand Up @@ -298,12 +371,12 @@ npx jscodeshift -t css-modules-v9-migration.js src/

## Version Comparison

| Feature | v8 (and earlier) | v9 |
|---------|-----------------|----|
| Feature | v8 (and earlier) | v9 |
|---------|-----------------|----|
| Default behavior | Default export object | Named exports |
| Import syntax | `import styles from '...'` | `import { className } from '...'` |
| Class reference | `styles.className` | `className` |
| Export convention | `asIs` (no transformation) | `camelCase` |
| Export convention | `asIs` (no transformation) | `camelCaseOnly` |
| TypeScript warnings | May show warnings | No warnings |
| Tree-shaking | Limited | Optimized |

Expand Down Expand Up @@ -368,13 +441,34 @@ Then search for `css-loader` options in the generated JSON file.

## Troubleshooting

### Build Error: exportLocalsConvention Incompatible with namedExport

If you see this error during build:
```
"exportLocalsConvention" with "camelCase" value is incompatible with "namedExport: true" option
```

**Cause:** Your webpack configuration has `namedExport: true` with `exportLocalsConvention: 'camelCase'`.

**Solution:** Change `exportLocalsConvention` to `'camelCaseOnly'` or `'dashesOnly'`:

```js
// config/webpack/commonWebpackConfig.js or similar
modules: {
namedExport: true,
exportLocalsConvention: 'camelCaseOnly' // or 'dashesOnly'
}
```

Alternatively, if you need the `'camelCase'` option (both original and camelCase exports), you must revert to v8 behavior by setting `namedExport: false` as shown in the "Reverting to Default Exports" section above.

### CSS Classes Not Applying

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

1. **Check import syntax**: Ensure you're using the correct import style for your configuration
2. **Verify class names**: Use `console.log` to see available classes
3. **Check camelCase conversion**: Kebab-case names are converted to camelCase in v9
3. **Check camelCase conversion**: Kebab-case names are converted to camelCase in v9 with `'camelCaseOnly'`
4. **Rebuild webpack**: Clear cache and rebuild: `rm -rf tmp/cache && bin/shakapacker`

### TypeScript Support
Expand Down Expand Up @@ -415,4 +509,4 @@ The configuration changes should not impact build performance significantly. If
- **v8 default**: Default export object with no conversion
- **Migration path**: Update imports or override configuration
- **Benefits of v9**: No warnings, better tree-shaking, explicit dependencies
- **Keeping v8 behavior**: Override css-loader configuration as shown above
- **Keeping v8 behavior**: Override css-loader configuration as shown above
60 changes: 57 additions & 3 deletions docs/v9_upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,18 @@ See the [TypeScript Documentation](./typescript.md) for usage examples.

### 1. CSS Modules Configuration Changed to Named Exports

**What changed:** CSS Modules are now configured with `namedExport: true` and `exportLocalsConvention: 'camelCase'` by default, aligning with Next.js and modern tooling standards.
**What changed:** CSS Modules are now configured with `namedExport: true` and `exportLocalsConvention: 'camelCaseOnly'` by default, aligning with Next.js and modern tooling standards.

> **Important:** When `namedExport: true` is enabled, css-loader requires `exportLocalsConvention` to be either `'camelCaseOnly'` or `'dashesOnly'`. Using `'camelCase'` will cause a build error: `"exportLocalsConvention" with "camelCase" value is incompatible with "namedExport: true" option`.

**Quick Reference: Configuration Options**

| Configuration | namedExport | exportLocalsConvention | CSS: `.my-button` | Export Available | Works With |
|---------------|-------------|------------------------|-------------------|------------------|------------|
| **v9 Default** | `true` | `'camelCaseOnly'` | `.my-button` | `myButton` only | ✅ Named exports |
| **Alternative** | `true` | `'dashesOnly'` | `.my-button` | `'my-button'` only | ✅ Named exports |
| **v8 Style** | `false` | `'camelCase'` | `.my-button` | Both `myButton` AND `'my-button'` | ✅ Default export |
| **❌ Invalid** | `true` | `'camelCase'` | - | - | ❌ Build Error |

**JavaScript Projects:**
```js
Expand Down Expand Up @@ -198,7 +209,7 @@ declare module '*.module.css' {

### Step 3: Handle Kebab-Case Class Names

v9 automatically converts kebab-case to camelCase:
v9 automatically converts kebab-case to camelCase with `exportLocalsConvention: 'camelCaseOnly'`:

```css
/* styles.module.css */
Expand All @@ -207,10 +218,34 @@ v9 automatically converts kebab-case to camelCase:
```

```js
// v9 imports
// v9 default - camelCase conversion
import { myButton, primaryColor } from './styles.module.css';
```

**Alternative: Keep kebab-case names with 'dashesOnly'**

If you prefer to keep kebab-case names in JavaScript, you can override the configuration to use `'dashesOnly'`:

```js
// config/webpack/commonWebpackConfig.js
modules: {
namedExport: true,
exportLocalsConvention: 'dashesOnly' // Keep original kebab-case names
}
```

Then use the original kebab-case names in your imports:

```js
// With dashesOnly configuration
import { 'my-button': myButton, 'primary-color': primaryColor } from './styles.module.css';
// or access as properties
import * as styles from './styles.module.css';
const buttonClass = styles['my-button'];
```

**Note:** With `'camelCaseOnly'` (default) or `'dashesOnly'`, only one version is exported. If you need both the original and camelCase versions, you would need to use `'camelCase'` instead, but this requires `namedExport: false` (v8 behavior). See the [CSS Modules Export Mode documentation](./css-modules-export-mode.md) for details on reverting to v8 behavior.

### Step 4: Update Configuration Files

If you have `webpack_loader` in your configuration:
Expand Down Expand Up @@ -253,6 +288,25 @@ Update your global type definitions as shown in Step 2.

If you see warnings about CSS module exports, ensure you've updated all imports to use named exports or have properly configured the override.

### Build Error: exportLocalsConvention Incompatible with namedExport

If you see this error:
```
"exportLocalsConvention" with "camelCase" value is incompatible with "namedExport: true" option
```

This means your webpack configuration has `namedExport: true` with `exportLocalsConvention: 'camelCase'`. The fix is to change to `'camelCaseOnly'` or `'dashesOnly'`:

```js
// config/webpack/commonWebpackConfig.js or wherever you configure css-loader
modules: {
namedExport: true,
exportLocalsConvention: 'camelCaseOnly' // or 'dashesOnly'
}
```

If you want to use `'camelCase'` (which exports both original and camelCase versions), you must set `namedExport: false` and revert to v8 behavior. See the [CSS Modules Export Mode documentation](./css-modules-export-mode.md) for details.

### Unexpected Peer Dependency Warnings After Upgrade

If you experience unexpected peer dependency warnings after upgrading to v9, you may need to clear your package manager's cache and reinstall dependencies. This ensures the new optional peer dependency configuration takes effect properly.
Expand Down
72 changes: 72 additions & 0 deletions lib/shakapacker/doctor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def perform_checks
# Dependency checks
check_javascript_transpiler_dependencies if config_exists?
check_css_dependencies
check_css_modules_configuration
check_bundler_dependencies if config_exists?
check_file_type_dependencies if config_exists?
check_sri_dependencies if config_exists?
Expand Down Expand Up @@ -440,6 +441,77 @@ def check_css_dependencies
check_optional_dependency("mini-css-extract-plugin", @warnings, "CSS extraction")
end

def check_css_modules_configuration
# Check for CSS module files in the project
return unless config_exists?

source_path = config.source_path
return unless source_path.exist?

# Performance optimization: Just check if ANY CSS module file exists
# Using .first with early return is much faster than globbing all files
css_module_exists = Dir.glob(File.join(source_path, "**/*.module.{css,scss,sass}")).first
return unless css_module_exists

# Check webpack configuration for CSS modules settings
webpack_config_paths = [
root_path.join("config/webpack/webpack.config.js"),
root_path.join("config/webpack/webpack.config.ts"),
root_path.join("config/webpack/commonWebpackConfig.js"),
root_path.join("config/webpack/commonWebpackConfig.ts")
]

webpack_config_paths.each do |config_path|
next unless config_path.exist?

config_content = File.read(config_path)

# Check for the invalid configuration: namedExport: true with exportLocalsConvention: 'camelCase'
if config_content.match(/namedExport\s*:\s*true/) && config_content.match(/exportLocalsConvention\s*:\s*['"]camelCase['"]/)
@issues << "CSS Modules: Invalid configuration detected in #{config_path.relative_path_from(root_path)}"
@issues << " Using exportLocalsConvention: 'camelCase' with namedExport: true will cause build errors"
@issues << " Change to 'camelCaseOnly' or 'dashesOnly'. See docs/v9_upgrade.md for details"
end

# Warn if CSS modules are used but no configuration is found
if !config_content.match(/namedExport/) && !config_content.match(/exportLocalsConvention/)
@info << "CSS module files found but no explicit CSS modules configuration detected"
@info << " v9 defaults: namedExport: true, exportLocalsConvention: 'camelCaseOnly'"
end
end

# Check for common v8 to v9 migration issues
check_css_modules_import_patterns
rescue => e
# Don't fail doctor if CSS modules check has issues
@warnings << "Unable to validate CSS modules configuration: #{e.message}"
end

def check_css_modules_import_patterns
# Look for JavaScript/TypeScript files that might have v8-style imports
source_path = config.source_path

# Use lazy evaluation with Enumerator to avoid loading all file paths into memory
# Stop after checking 50 files or finding a match
v8_pattern = /import\s+\w+\s+from\s+['"][^'"]*\.module\.(css|scss|sass)['"]/

Dir.glob(File.join(source_path, "**/*.{js,jsx,ts,tsx}")).lazy.take(50).each do |file|
# Read file and check for v8 pattern
content = File.read(file)

# Check for v8 default import pattern with .module.css
if v8_pattern.match?(content)
@warnings << "Potential v8-style CSS module imports detected (using default import)"
@warnings << " v9 uses named exports. Update to: import { className } from './styles.module.css'"
@warnings << " Or use: import * as styles from './styles.module.css' (TypeScript)"
@warnings << " See docs/v9_upgrade.md for migration guide"
break # Stop after finding first occurrence
end
end
rescue => e
# Don't fail doctor if import pattern check has issues
end

def check_bundler_dependencies
bundler = config.assets_bundler
case bundler
Expand Down
9 changes: 7 additions & 2 deletions package/utils/getStyleRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ const getStyleRule = (test: RegExp, preprocessors: any[] = []): StyleRule | null
sourceMap: true,
importLoaders: 2,
modules: {
auto: true
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'
}
}
},
Expand All @@ -56,4 +61,4 @@ const getStyleRule = (test: RegExp, preprocessors: any[] = []): StyleRule | null
return null
}

export = { getStyleRule }
export = { getStyleRule }
Loading