feat: add pure parser option for css/module and css/auto#20946
Conversation
🦋 Changeset detectedLatest commit: 8ed650f The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Pull request overview
This PR introduces a new parser.pure option for css/module and css/auto module types to enforce “pure selector” semantics (every selector must include at least one local class or id), aligning webpack’s built-in CSS Modules behavior with postcss-modules-local-by-default pure mode.
Changes:
- Add
pureparser option support inlib/css/CssParser.js, including error emission and comment-based opt-outs (cssmodules-pure-ignore,cssmodules-pure-no-check). - Extend public typings and JSON schemas to expose/validate the new option for
css/moduleandcss/auto. - Add/adjust config-case tests to cover pure-mode behavior and expected errors.
Reviewed changes
Copilot reviewed 18 out of 22 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
lib/css/CssParser.js |
Implements pure-mode selector tracking, opt-out comments, and build errors for impure selectors. |
lib/css/CssModulesPlugin.js |
Switches css/module + css/auto parser option validation to the new schema/type that includes pure. |
schemas/WebpackOptions.json |
Adds CssParserPure and CssAutoOrModuleParserOptions schema definitions; wires them into parser options by module type. |
schemas/plugins/css/CssAutoOrModuleParserOptions.json |
Adds plugin schema entry for the new parser-options shape. |
schemas/plugins/css/CssAutoOrModuleParserOptions.check.js |
Generated runtime validator for the new schema. |
schemas/plugins/css/CssAutoOrModuleParserOptions.check.d.ts |
Typings for the generated validator. |
declarations/WebpackOptions.d.ts |
Adds CssAutoOrModuleParserOptions + CssParserPure to public declarations. |
types.d.ts |
Updates generated top-level typings to expose pure for css/auto and css/module. |
test/configCases/css/pure-parser-options/webpack.config.js |
New test case enabling parser.pure for css/module. |
test/configCases/css/pure-parser-options/index.js |
Asserts locals exports for pure-compliant modules and file-level no-check behavior. |
test/configCases/css/pure-parser-options/valid.module.css |
Pure-compliant selector fixtures, including ignore-comment coverage. |
test/configCases/css/pure-parser-options/no-check.module.css |
Fixture for file-leading cssmodules-pure-no-check. |
test/configCases/css/pure-parser-options/invalid.module.css |
Fixture containing impure selectors expected to error. |
test/configCases/css/pure-parser-options/errors.js |
Expected error patterns for the new config case. |
test/configCases/css/postcss-modules-plugins/webpack.config.js |
Enables parser.pure for the existing “pure” fixture in this config case. |
test/configCases/css/postcss-modules-plugins/postcss-modules-local-by-default.pure.modules.css |
Removes an outdated “TODO not implemented yet” comment. |
test/configCases/css/postcss-modules-plugins/errors.js |
Adds expected pure-mode error patterns for this config case. |
test/configCases/css/postcss-modules-plugins/__snapshots__/ConfigTest.snap |
Snapshot update reflecting removal of the TODO comment. |
test/configCases/css/postcss-modules-plugins/__snapshots__/ConfigCacheTest.snap |
Cache snapshot update reflecting removal of the TODO comment. |
cspell.json |
Adds spellchecker allowlist entries for cssmodules and PCSL. |
.changeset/css-modules-pure-parser-option.md |
Adds a changeset documenting the new parser option and comment semantics. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pureBlockStack.push({ | ||
| ignored: pureIgnorePending, | ||
| skipOwn: inheritedSkip, | ||
| skipChildren: nextBlockChildrenSkip || inheritedSkip, | ||
| treatAsLeaf: nextBlockTreatAsLeaf, | ||
| ancestorHadLocal: parentEffectivePure() || lastSelectorHadLocal, |
| } | ||
| inAtRulePrelude = false; | ||
| isNextRulePrelude = isNextNestedSyntax(input, end); |
| if (pureMode) { | ||
| if (PURE_IGNORE_RE.test(value)) { | ||
| pureIgnorePending = true; | ||
| } else if ( | ||
| PURE_NO_CHECK_RE.test(value) && | ||
| scope === CSS_MODE_TOP_LEVEL && | ||
| !seenTopLevelRule | ||
| ) { | ||
| pureNoCheck = true; | ||
| } |
| const isRulePrelude = isNextRulePrelude; | ||
| const lastSelectorHadLocal = currentSelectorHasLocal; | ||
| if (isRulePrelude) finalizeSelector(); | ||
| const top = pureTop(); | ||
| if (top) top.hasNestedBlock = true; | ||
| const inheritedSkip = top ? top.skipChildren : false; | ||
| pureBlockStack.push({ | ||
| ignored: pureIgnorePending, | ||
| skipOwn: inheritedSkip, | ||
| skipChildren: nextBlockChildrenSkip || inheritedSkip, | ||
| treatAsLeaf: nextBlockTreatAsLeaf, | ||
| ancestorHadLocal: parentEffectivePure() || lastSelectorHadLocal, | ||
| impure: isRulePrelude && currentRuleHasImpureSelector, | ||
| hasDirectDecl: false, | ||
| hasNestedBlock: false, | ||
| isRulePrelude, | ||
| preludeStart: currentRulePreludeStart, | ||
| preludeEnd: start | ||
| }); | ||
| pureIgnorePending = false; | ||
| nextBlockChildrenSkip = false; | ||
| nextBlockTreatAsLeaf = false; | ||
| currentRuleHasImpureSelector = false; | ||
| currentSelectorHasLocal = false; |
Implements postcss-modules-local-by-default (PCSL) v4.2.0 pure-mode semantics in webpack's built-in CSS modules parser: - Every selector must contain at least one local class or id, otherwise the rule errors at build. Available on `css/module` and `css/auto` parser options; not exposed for `css/global` since pure mode is the opposite of the global-by-default semantic of that type. - `/* cssmodules-pure-ignore */` directly before a rule suppresses that rule's check (per-rule only; does NOT propagate to nested rules). - `/* cssmodules-pure-no-check */` placed among the leading comments of the file (before any rule or at-rule) disables the check for the whole file. - Nested rules inside a local-bearing ancestor are treated as pure- compliant; `&` resolves to the parent rule's overall purity. - `@keyframes` / `@counter-style` body contents are exempt; the at-rule itself still errors when its identifier is `:global(...)`. - Rules whose body contains only nested rules don't trigger the check (children carry it instead, matching PCSL's `isNodeWithoutDeclarations`). - At-rules' preludes (e.g. `min-width` inside `@media (...)`) aren't mistaken for declarations. Addresses Copilot review feedback on PR #20946: - #1: `ancestorHadLocal` now derives from "this rule is fully pure" (no impure comma-segment) rather than "last selector had a local", so `:global(.a), .b { span { ... } }` correctly throws on `span`. - #2: `currentRulePreludeStart` now advances on top-level `;` and on at-rules that consume their own `;` (e.g. `@import`), so a later impure rule's reported selector doesn't include the preceding text. - #3: `seenTopLevelRule` is set on any non-comment top-level node (including `;`-terminated at-rules), matching PCSL's `isPureCheckDisabled` leading-comments rule. - #4: All pure-mode bookkeeping (stack push/pop, `currentSelectorHasLocal` writes, etc.) is gated behind `pureMode`, so CSS modules without `parser.pure` pay no overhead. Test coverage: dedicated `test/configCases/css/pure-parser-options/` plus the existing `postcss-modules-plugins` PCSL fixture enabled with `parser: { pure: true }` and a full `errors.js` (43 expected throws). https://claude.ai/code/session_014jWnrU38fbFtCwKGtnnBig
fcfdab3 to
b2fea1e
Compare
|
This PR is packaged and the instant preview is available (d689afd). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@d689afd
yarn add -D webpack@https://pkg.pr.new/webpack@d689afd
pnpm add -D webpack@https://pkg.pr.new/webpack@d689afd |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #20946 +/- ##
==========================================
- Coverage 91.37% 91.37% -0.01%
==========================================
Files 569 569
Lines 56993 57129 +136
Branches 15174 15233 +59
==========================================
+ Hits 52075 52199 +124
- Misses 4918 4930 +12
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| parser: { | ||
| pure: true | ||
| }, | ||
| type: "css/module" |
- `RegExp.prototype.exec` returns `RegExpExecArray | null`; cast to the non-null form so `lint:types` is clean (the regex always matches an empty string, so the result is non-null). - `getArguments()` snapshot now includes the new `module.parser.css/auto.pure` and `module.parser.css/module.pure` CLI flags. https://claude.ai/code/session_014jWnrU38fbFtCwKGtnnBig
Addresses Copilot review on b2fea1e: the `pure` parser option is documented for both `css/module` and `css/auto`, but the config-case only exercised `css/module`. Adds three fixtures matched by the same `type: "css/auto"` rule: - `auto-pure.module.css` — `.module.css` filename → treated as a CSS module → pure check applies → passes (only a local class). - `auto-impure.module.css` — `.module.css` filename → pure check applies → throws on `body`. - `auto-non-module.css` — no `.module.` in the filename → `css/auto` treats it as plain CSS → pure check must NOT apply, even though the rule sets `pure: true`. Verified via the absence of throws on `body`/`:global(.global-only)`. `errors.js` adds the expected `body` throw under `auto-impure.module.css`; `index.js` adds an assertion that the local from `auto-pure.module.css` is exported under the auto-resolved CSS-module name. https://claude.ai/code/session_014jWnrU38fbFtCwKGtnnBig
Types CoverageCoverage after merging claude/css-modules-pure-option-dFJZj into main will be
Coverage Report
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Webpack 5.107 adds a new pure parser option for css/module and css/auto that mirrors postcss-modules-local-by-default's pure mode. Every selector must contain at least one local class or id, otherwise the build fails. Documents the option, plus the two opt-out comments (/* cssmodules-pure-ignore */ per-rule and /* cssmodules-pure-no-check */ per-file) and the constructs that are exempt by design. Refs: webpack/webpack#20946
Mirrors the pure mode from css-loader / icss-utils: every selector must
contain at least one local class or id, otherwise webpack emits a build
error. Not exposed for css/global since pure is the opposite of the
global-by-default semantic of that type.
https://claude.ai/code/session_014jWnrU38fbFtCwKGtnnBig