Skip to content

feat(css): scope counter names in CSS modules - #21600

Merged
alexander-akait merged 3 commits into
mainfrom
feat/css-modules-counter-scoping
Aug 4, 2026
Merged

feat(css): scope counter names in CSS modules#21600
alexander-akait merged 3 commits into
mainfrom
feat/css-modules-counter-scoping

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

Native CSS already scopes @keyframes / @counter-style / @container / view-transition names, but counter names were left global, so two modules that both write counter-reset: section collide on a document-global counter. Worse, @counter-style preludes are renamed while counter(x, thumbs) was not, so a scoped custom counter style silently stopped applying — Lightning CSS has the same bug. This scopes counter names and their counter() / counters() / target-counter() references under customIdents, leaving UA counters (list-item, page, pages) global.

It also moves the parser's hand-listed keyword tables (55 predefined counter styles, the CSS-wide keywords, and the animation / container / grid / view-transition tables) into tooling/generate-css-data.js, derived from each property's own mdn-data grammar instead of typed out. That drops 316 lines from lib/css/CssParser.js and fixes animation: 3s auto, where auto names the timeline and was being localized as a keyframes name. Refs #14893.

What kind of change does this PR introduce?

feat

Did you add tests for your changes?

Yes — test/configCases/css/counter-scoping (declaration + reference scoping, counter-style argument, reversed(), @value aliases, dashed names, reserved keywords, global() / local() escape hatches; dev, production and customIdents: false), plus an animation: 3s auto fixture in test/configCases/css/css-modules.

Does this PR introduce a breaking change?

No API break, but a behavior change: counter names in CSS modules are now localized by default, like @keyframes. A file that needs a shared counter should wrap the name in :global(...) / global(...) or set parser: { customIdents: false }. animation: … auto is no longer exported as a keyframes name.

If relevant, what needs to be documented once your changes are merged or what have you already documented?

The examples/css native-CSS scoping reference now lists counter names. The Native CSS guide on webpack.js.org may want the same note.

Use of AI

Yes. Implemented with AI assistance (Claude Code): a cross-bundler audit of CSS Modules support (running lightningcss and postcss-modules directly to confirm the gap and the shared counter() bug), the lib/css/CssParser.js scoping changes, the grammar-derived keyword tables in tooling/generate-css-data.js, and the tests. The derived tables were diffed entry-by-entry against the previous hand-written ones (499 keywords, 16 intentional differences, each traced) before landing. All changes were verified locally: the CSS config cases, stats and unit suites pass, yarn fix:special regenerates clean and idempotent, and tsc / ESLint / Prettier / cspell are clean.


Generated by Claude Code


Note

Medium Risk
Behavior change for CSS modules using custom counter names (now scoped by default) and for animation shorthand parsing; well-covered by tests but affects emitted CSS for existing module stylesheets.

Overview
Extends native CSS module scoping so counter names (counter-reset / -increment / -set and counter() / counters() / target-counter() / target-counters()) localize under customIdents, matching @counter-style and fixing mismatched @counter-style vs counter(x, style) renames. UA keywords (list-item, page, predefined styles, etc.) stay global; dashed names and global() / local() behave like other custom idents.

Parser data refactor: inline keyword tables in CssParser.js move into CSS_MODULES_KEYWORDS / CSS_MODULES_KEYWORD_OPTIONS in lib/css/data.js (grammar-derived via tooling/generate-css-data.js). Known-property lookup uses Map values instead of plain objects. animation: 3s auto no longer treats auto as a keyframes name (timeline keyword).

Adds counter-scoping config tests and an animationTimeline fixture; docs/changeset note counter scoping.

Reviewed by Cursor Bugbot for commit 52e411d. Bugbot is set up for automated code reviews on this repo. Configure here.

Localize `counter-reset` / `counter-increment` / `counter-set` names and
their `counter()` / `counters()` / `target-counter()` references under
`customIdents`, so two modules can no longer collide on a document-global
counter. UA counters (`list-item`, `page`, `pages`) stay global.

Also rewrite the `<counter-style>` argument of `counter()` / `counters()`:
`@counter-style` preludes were already renamed, so a reference there kept
naming the original and the custom style silently stopped applying.
The parser hand-listed the keywords of every property a `css/module` reads
a scoped name out of — 55 predefined counter styles, the CSS-wide keywords
and the animation / container / grid / view-transition / counter tables.
Read them off each property's own grammar in the generator instead, so a
spec change becomes a reviewable diff rather than a stale list, and emit
them into `lib/css/data.js`.

What the grammars do not state stays in `SUPPLEMENT`, each entry carrying
its reason: the predefined counter styles and UA counters (`mdn-data`
models both as a bare `<custom-ident>`), `@view-transition-group` and
`masonry` (published grammars lag the spec), and the `grid` shorthand's
own `auto-flow` keywords.

Deriving the tables also fixes `animation: 3s auto`, where `auto` names
the timeline and was localized as a keyframes name.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

This PR is packaged and the instant preview is available (d6fbda7).

Install it locally:

  • npm
npm i -D webpack@https://pkg.pr.new/webpack@d6fbda7
  • yarn
yarn add -D webpack@https://pkg.pr.new/webpack@d6fbda7
  • pnpm
pnpm add -D webpack@https://pkg.pr.new/webpack@d6fbda7

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.51477% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.11%. Comparing base (4c2279d) to head (52e411d).

Files with missing lines Patch % Lines
lib/css/CssParser.js 88.76% 10 Missing ⚠️
tooling/generate-css-data.js 97.91% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #21600      +/-   ##
==========================================
- Coverage   94.11%   94.11%   -0.01%     
==========================================
  Files         628      628              
  Lines       78139    78332     +193     
  Branches    22944    23047     +103     
==========================================
+ Hits        73541    73719     +178     
- Misses       4598     4613      +15     
Flag Coverage Δ
css-parsing 24.85% <25.80%> (-0.04%) ⬇️
html5lib 26.19% <6.45%> (-0.03%) ⬇️
integration 89.33% <89.24%> (-0.01%) ⬇️
test262 42.79% <6.45%> (-0.04%) ⬇️
unit 49.45% <62.02%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…xture

`ConfigCacheTestCases` keeps its own snapshots of the `css-modules` and
`pure-css` stylesheets, so the `animation: 3s auto` fixture has to land in
both suites.
@changeset-bot

changeset-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 52e411d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack Minor

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

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging feat/css-modules-counter-scoping into main will be
99.31%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
bin
   webpack.js98.82%100%100%98.82%103
examples
   build-common.js100%100%100%100%
   buildAll.js100%100%100%100%
   examples.js100%100%100%100%
   template-common.js98.21%100%100%98.21%72
examples/custom-javascript-parser
   test.filter.js100%100%100%100%
examples/custom-javascript-parser/internals
   acorn-parse.js100%100%100%100%
   meriyah-parse.js100%100%100%100%
   oxc-parse.js100%100%100%100%
examples/markdown
   webpack.config.mjs100%100%100%100%
examples/module-federation
   test.filter.js100%100%100%100%
examples/reexport-components
   test.filter.js100%100%100%100%
examples/typescript
   test.filter.js100%100%100%100%
examples/typescript-non-erasable
   test.filter.js50%100%100%50%5
examples/virtual-modules
   test.filter.js100%100%100%100%
examples/wasm-bindgen-esm
   test.filter.js100%100%100%100%
examples/wasm-complex
   test.filter.js100%100%100%100%
examples/wasm-emscripten
   test.filter.js100%100%100%100%
examples/wasm-simple
   test.filter.js100%100%100%100%
examples/wasm-simple-source-phase
   test.filter.js100%100%100%100%
lib
   APIPlugin.js100%100%100%100%
   AsyncDependenciesBlock.js100%100%100%100%
   AutomaticPrefetchPlugin.js100%100%100%100%
   BannerPlugin.js100%100%100%100%
   Cache.js98.21%100%100%98.21%101
   CacheFacade.js100%100%100%100%
   Chunk.js99.72%100%100%99.72%39
   ChunkGraph.js100%100%100%100%
   ChunkGroup.js100%100%100%100%
   ChunkTemplate.js100%100%100%100%
   CircularModulesPlugin.js98.81%100%100%98.81%136
   CleanPlugin.js99.13%100%100%99.13%214, 234
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.42%100%100%98.42%1648, 1967, 1974, 1982, 2004, 2007, 2946, 3425–3426, 3458, 4162, 4192, 4245–4246, 4250, 4255, 4271–4272, 4286–4287, 4292–4293, 4773, 4799, 527, 532, 5607, 5639, 5656, 5672, 5688, 5703, 5728–5729, 5731, 6061, 6066, 6072, 6075, 6082, 6094, 6096, 6100, 6116, 6131, 6163, 6217, 6241, 6356, 778–779
   Compiler.js99.56%100%100%99.56%1159–1160, 1168
   ConcatenationScope.js99.12%100%100%99.12%300
   ConditionalInitFragment.js100%100%100%100%
   ConstPlugin.js100%100%100%100%
   ContextExclusionPlugin.js100%100%100%100%
   ContextModule.js99.88%100%100%99.88%1461
   ContextModuleFactory.js97.20%100%100%97.20%266, 435, 456, 461, 501, 512, 514, 518, 527–528
   ContextReplacementPlugin.js100%100%100%100%
   DefinePlugin.js99.08%100%100%99.08%1080, 176–177, 193, 212, 286
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.51%100%100%98.51%480, 527
   DependencyTemplate.js100%100%100%100%
   DependencyTemplates.js100%100%100%100%
   DotenvPlugin.js98.41%100%100%98.41%378, 391–392
   DynamicEntryPlugin.js100%100%100%100%
   EntryOptionPlugin.js100%100%100%100%
   EntryPlugin.js100%100%100%100%
   Entrypoint.js100%100%100%100%
   EnvironmentPlugin.js97.14%100%100%97.14%49
   ErrorHelpers.js100%100%100%100%
   EvalDevToolModulePlugin.js100%100%100%100%
   EvalSourceMapDevToolPlugin.js100%100%100%100%
   ExportsInfo.js100%100%100%100%
   ExportsInfoApiPlugin.js100%100%100%100%
   ExternalModule.js98.67%100%100%98.67%1216, 1219, 515–519, 521, 667
   ExternalModuleFactoryPlugin.js100%100%100%100%
   ExternalsPlugin.js100%100%100%100%
   FileSystemInfo.js99.16%100%100%99.16%1267, 1269–1274, 1281, 1284, 182, 2502–2503, 2506, 2517, 2528, 2539, 280, 3976, 3991, 4015
   FlagAllModulesAsUsedPlugin.js100%100%100%100%
   FlagDependencyExportsPlugin.js98.21%100%100%98.21%448, 457, 460, 464, 476
   FlagDependencyUsagePlugin.js100%100%100%100%
   FlagEntryExportAsUsedPlugin.js100%100%100%100%
   Generator.js100%100%100%100%
   HotModuleReplacementPlugin.js100%100%100%100%
   HotUpdateChunk.js100%100%100%100%
   IgnorePlugin.js100%100%100%100%
   IgnoreWarningsPlugin.js100%100%100%100%
   InitFragment.js100%100%100%100%
   JavascriptMetaInfoPlugin.js100%100%100%100%
   LazyBarrel.js100%100%100%100%
   LibraryTemplatePlugin.js100%100%100%100%
   LoaderOptionsPlugin.js100%100%100%100%
   LoaderTargetPlugin.js100%100%100%100%
   MainTemplate.js100%100%100%100%
   ManifestPlugin.js100%100%100%100%
   Module.js98.51%100%100%98.51%1303, 1308, 1368, 1382, 1444, 1453
   ModuleFactory.js100%100%100%100%
   ModuleFilenameHelpers.js98.90%100%100%98.90%111, 113
   ModuleGraph.js99.78%100%100%99.78%1170
   ModuleGraphConnection.js100%100%100%100%
   ModuleInfoHeaderPlugin.js100%100%100%100%
   ModuleNotFoundError.js100%100%100%100%
   ModuleProfile.js100%100%100%100%
   ModuleSourceTypeConstants.js100%100%100%100%
   ModuleTemplate.js100%100%100%100%
   ModuleTypeConstants.js100%100%100%100%
   MultiCompiler.js99.71%100%100%99.71%687
   MultiStats.js100%100%100%100%
   MultiWatching.js100%100%100%100%
   NoEmitOnErrorsPlugin.js100%100%100%100%
   NodeStuffPlugin.js100%100%100%100%
   NormalModule.js97.98%100%100%97.98%1020, 1023, 1040, 1057, 1305, 1339, 1355, 1803, 2100, 2105–2115, 34
   NormalModuleFactory.js98.72%100%100%98.72%1123, 1391, 1402, 1412, 1463–1465, 1472, 526, 538
   NormalModuleReplacementPlugin.js100%100%100%100%
   NullFactory.js100%100%100%100%
   OptimizationStages.js100%100%100%100%
   OptionsApply.js100%100%100%100%
   Parser.js100%100%100%100%
   PlatformPlugin.js100%100%100%100%
   PrefetchPlugin.js100%100%100%100%
   ProgressPlugin.js99.80%100%100%99.80%691
   ProvidePlugin.js100%100%100%100%
   RawModule.js100%100%100%100%
   RecordIdsPlugin.js100%100%100%100%
   RequestShortener.js100%100%100%100%
   ResolverFactory.js100%100%100%100%
   RuntimeGlobals.js100%100%100%100%
   RuntimeModule.js100%100%100%100%
   RuntimePlugin.js100%100%100%100%
   RuntimeTemplate.js100%100%100%100%
   SelfModuleFactory.js100%100%100%100%
   SingleEntryPlugin.js100%100%100%100%
   SourceMapDevToolModuleOptionsPlugin.js100%100%100%100%
   SourceMapDevToolPlugin.js98.63%100%100%98.63%220, 224, 226, 420, 431, 890
   Stats.js100%100%100%100%
   Template.js100%100%100%100%
   TemplatedPathPlugin.js99.47%100%100%99.47%352–353
   UseStrictPlugin.js100%100%100%100%
   WarnCaseSensitiveModulesPlugin.js100%100%100%100%
   WarnDeprecatedOptionPlugin.js100%100%100%100%
   WarnNoModeSetPlugin.js100%100%100%100%
   WatchIgnorePlugin.js100%100%100%100%
   Watching.js100%100%100%100%
   WebpackError.js100%100%100%100%
   WebpackIsIncludedPlugin.js100%100%100%100%
   WebpackOptionsApply.js100%100%100%100%
   WebpackOptionsDefaulter.js100%100%100%100%
   buildChunkGraph.js99.87%100%100%99.87%371
   cli.js98.63%100%100%98.63%10, 119, 549, 581, 631, 905
   index.js99.73%100%100%99.73%184
   validateSchema.js94.67%100%100%94.67%100, 87, 89, 98
   webpack.js97.10%100%100%97.10%10, 263, 285, 287
lib/asset
   AssetBytesGenerator.js100%100%100%100%
   AssetBytesParser.js100%100%100%100%
   AssetGenerator.js100%100%100%100%
   AssetModule.js100%100%100%100%
   AssetModulesPlugin.js97.95%100%100%97.95%295, 319, 322, 42, 452, 47
   AssetParser.js100%100%100%100%
   AssetSourceGenerator.js100%100%100%100%
   AssetSourceParser.js100%100%100%100%
   RawDataUrlModule.js100%100%100%100%
   WebManifestGenerator.js100%100%100%100%
   WebManifestParser.js100%100%100%100%
lib/async-modules
   AsyncModuleHelpers.js100%100%100%100%
   AwaitDependenciesInitFragment.js100%100%100%100%
   InferAsyncModulesPlugin.js100%100%100%100%
   isGeneratorLowered.js100%100%100%100%
lib/bun
   BunTargetPlugin.js100%100%100%100%
lib/cache
   AddBuildDependenciesPlugin.js100%100%100%100%
   AddManagedPathsPlugin.js100%100%100%100%
   IdleFileCachePlugin.js97.92%100%100%97.92%75, 87, 95
   MemoryCachePlugin.js95.83%100%100%95.83%33
   MemoryWithGcCachePlugin.js93.15%100%100%93.15%107, 114–115, 123, 90
   PackFileCacheStrategy.js96.52%100%100%96.52%1310, 1410, 1414, 1476, 1712, 1796, 1819, 1851, 675, 694, 704–706, 708, 724–725, 730, 733, 735, 740, 745, 770, 776, 810, 816, 822, 827, 838, 847, 852–853, 855, 872, 878–879, 881
   ResolverCachePlugin.js100%100%100%100%
   getLazyHashedEtag.js100%100%100

@codspeed-hq

codspeed-hq Bot commented Aug 4, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 22.16%

⚡ 1 improved benchmark
✅ 219 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "many-modules-esm", scenario '{"name":"mode-production","mode":"production"}' 11.1 MB 9.1 MB +22.16%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing feat/css-modules-counter-scoping (52e411d) with main (4c2279d)

Open in CodSpeed

@alexander-akait
alexander-akait merged commit d6fbda7 into main Aug 4, 2026
62 of 63 checks passed
@alexander-akait
alexander-akait deleted the feat/css-modules-counter-scoping branch August 4, 2026 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Sponsor
SponsoredKunjungi sekarang
Promo