cloudflare / cloudflare/vinext

CSS Modules: `@extend` (postcss-extend-rule) silently dropped — CSS-Modules plugin runs before user PostCSS plugins

Open
#2,992 5 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
8.8k
Forks
406
Avg merge
2d 6h
Merged PRs (30d)
120

Description

## Problem

`@extend` from `postcss-extend-rule` is silently discarded in CSS Modules. Every declaration the rule should have inherited is missing — in dev and in the production build alike — with no warning or error.

```css
/* styles.module.css */
.shared { position: absolute; visibility: hidden; }
.wrap { @extend .shared; border: 1px solid red; }
```

Expected: `.wrap` inherits `position: absolute; visibility: hidden`.

Actual output:

```css
._shared_1u9uh_1{visibility:hidden;position:absolute}
._wrap_1u9uh_2{border:1px solid red}
```

The at-rule is gone and nothing was copied.

## Cause

Vite's `compileCSS` puts the CSS-Modules plugin at the **front** of the PostCSS chain:

```js
if (isModule) postcssPlugins.unshift((await importPostcssModules()).default({ ... }))
```

So class names are already hashed to `._shared_1u9uh_1` by the time the project's `postcss-extend-rule` runs. `@extend .shared` matches no rule, and the plugin drops it silently.

Running the *same* PostCSS config standalone over the same file resolves the extend correctly, so the configuration is fine — only the ordering inside Vite's pipeline is wrong.

## Reproduction (no vinext involved)

```
postcss.config.mjs -> export default { plugins: { 'postcss-extend-rule': {} } }
src/styles.module.css -> the two rules above
src/main.js -> import s from "./styles.module.css"
vite build
```

The emitted CSS shows `_wrap_` without the extended declarations. This reproduces on plain Vite with no vinext in the project, so the underlying issue is Vite's plugin ordering — filing here because vinext already resolves and injects the project's PostCSS config, and is where a Next-compatible pipeline would be expected to handle it (webpack/`css-loader` runs the user's PostCSS before CSS-Modules scoping, so the same stylesheet works under `next build`).

## Impact

Any CSS module using `@extend` loses those declarations. In one Pages Router app this silently affected **46 `@extend` uses across 11 module files**, producing components with missing positioning — dropdown panels rendering in flow instead of as absolutely-positioned overlays, etc. Because nothing warns, it presents as "the CSS is just wrong" with no obvious cause.

## Workaround

Expanding `@extend` before Vite's pipeline sees the file, via an `enforce: "pre"` transform, fixes it — the extend resolves against the original selectors and Vite then hashes the already-expanded rules:

```ts
const expandCssModuleExtends = () => {
let processor;
return {
name: "expand-css-module-extends",
enforce: "pre" as const,
async transform(code: string, id: string) {
const file = id.split("?")[0];
if (!file.endsWith(".module.css") || !code.includes("@extend")) return null;
if (!processor) {
const postcss = (await import("postcss")).default;
const extendRule = (await import("postcss-extend-rule")).default;
const presetEnv = (await import("postcss-preset-env")).default;
// `@extend` must see flat selectors: postcss-extend-rule skips nested
// rules whose selector it cannot resolve, so flatten nesting first.
processor = postcss([
presetEnv({ stage: false, features: { "nesting-rules": true } }),
extendRule(),
]);
}
const result = await processor.process(code, { from: file });
return { code: result.css, map: null };
},
};
};
```

Note the nesting step — without it only the `@extend`s at one nesting depth are expanded, which looks like a partial fix.

## Environment

- vinext `1.0.0-beta.6`
- Vite `8.1.0`
- `postcss-extend-rule`, `postcss-preset-env`
- Reproduced in dev and in the production build, and on plain Vite without vinext

Contributor guide

Open the contributing guide

Research direction

Start at Vite's compileCSS implementation and reproduce the issue with the minimal postcss.config.mjs, styles.module.css, and src/main.js setup described here. Trace how CSS Modules and user PostCSS plugins are ordered, then verify that @extend declarations survive in both dev and production builds, including nested rules.

Written by the indexing model from the issue text.

Assessment

Tech stack
css, typescript, vite
Domain
build-system, frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.