feat: scope view-transition names in CSS modules - #21486
Conversation
Export the names of global class and id selectors (`:global(.foo)`, `:global(#bar)`, bare `:global .baz`) as ICSS exports mapped to their unscoped names, matching css-loader's `exportGlobals`. Off by default; only local selectors are exported unless enabled.
Localize `view-transition-name` / `view-transition-group` / `view-transition-class` custom-ident values and the matching `::view-transition-group()` / `-image-pair()` / `-old()` / `-new()` pseudo-element name arguments under the `customIdents` parser option, so a transition name and its pseudo references resolve to the same scoped identifier. Dashed-ident names keep scoping under `dashedIdents`.
Demonstrate a small plugin that reads each CSS module's `buildInfo.cssData.exports` (original name -> generated scoped name) and writes it to a JSON sidecar — the native-CSS equivalent of the postcss-modules `getJSON` callback.
Drop the `exportGlobals` option: it only exports global class/id names to their own literal string (marginal value), and the modern engine (Lightning CSS) has no equivalent — it is a postcss-modules-only concept. Keeps the CSS-modules surface aligned with Lightning CSS.
Add a scoping registry (lib/css/README.md) listing what native CSS localizes, what the blanket dashed-ident scanner auto-covers, and what is intentionally left global, plus a decision guide for new CSS features. Add regression tests asserting anchor-scope, @container style() queries, and @function scope consistently via the dashed-ident scanner.
Merge the separate css-module-exports example into the canonical css example and move the scoping reference out of lib into that example's README (template). One CSS example now shows native CSS, the getJSON-equivalent name-map plugin, and what native CSS scopes.
Extend the example plugin to emit a `.d.ts` per CSS module (alongside the JSON name map) from `buildInfo.cssData.exports`, so imports of the module are typed. No bundler ships native CSS-module `.d.ts` generation; the map webpack already computes makes it a few lines. Includes how to wire it for editor pickup.
🦋 Changeset detectedLatest commit: 3bc5cac 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 |
|
This PR is packaged and the instant preview is available (828a65c). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@828a65c
yarn add -D webpack@https://pkg.pr.new/webpack@828a65c
pnpm add -D webpack@https://pkg.pr.new/webpack@828a65c |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #21486 +/- ##
=======================================
Coverage 93.50% 93.50%
=======================================
Files 617 619 +2
Lines 72875 72956 +81
Branches 20972 21000 +28
=======================================
+ Hits 68140 68221 +81
Misses 4735 4735
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Emitting the declaration into dist is not usable for typing; write it beside the CSS file (style.module.css.d.ts) so editors and tsc pick it up with no extra config. Commit the generated declaration or gitignore it.
Merging this PR will improve performance by 20.23%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ⚡ | Memory | benchmark "many-chunks-esm", scenario '{"name":"mode-production","mode":"production"}' |
9 MB | 7.5 MB | +20.28% |
| ⚡ | Memory | benchmark "devtool-eval", scenario '{"name":"mode-production","mode":"production"}' |
8 MB | 6.6 MB | +20.17% |
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 claude/css-modules-missing-features-hlk1lr (3bc5cac) with main (27dfadb)
Switch the css example's declaration generator to the object form that
typed-css-modules and similar tools emit (`declare const styles: { readonly
"key": string }; export default styles;`). Quoted keys handle kebab-case
and reserved words like `default` that the earlier `export const name`
form produced invalid TypeScript for. Pair it with `namedExports: false`
so the module's default export is the object the declaration types.
Types CoverageCoverage after merging claude/css-modules-missing-features-hlk1lr into main will be
Coverage Report |
Summary
Native CSS modules already scope
@keyframes/@counter-style/@containernames, butview-transition-name/-group/-classcustom-ident values and their::view-transition-*()pseudo-element references were left unscoped, so a scoped transition never matched (the declaration and the pseudo used different names). This scopes them under thecustomIdentsparser option so a transition name and its::view-transition-group()/-image-pair()/-old()/-new()references resolve to the same identifier; dashed-ident names keep scoping underdashedIdents. This brings native CSS to parity with Lightning CSS here.It also adds an example under
examples/cssdemonstrating how to emit the CSS Modules name map as JSON and generate a TypeScript.d.ts(typed CSS modules) frommodule.buildInfo.cssData.exports, plus regression tests locking in that the blanket dashed-ident scanner covers emerging features (anchor-scope,@container style(),@function). Refs #14893.What kind of change does this PR introduce?
feat
Did you add tests for your changes?
Yes —
test/configCases/css/view-transitions(declaration + pseudo scoping, consistency, dashed form, reserved keywords; dev and production),test/configCases/css/dashed-idents-scoping(locks in dashed-ident coverage ofanchor-scope/@container style()/@function), and updatedtest/configCases/css/postcss-modules-pluginssnapshots.Does this PR introduce a breaking change?
No API break, but a behavior change:
view-transition-name/-group/-classin CSS modules are now localized by default (like@keyframes). A file that intentionally uses a global view-transition name should wrap it in:global(...)or setparser: { customIdents: false }. Cross-document (MPA) transitions that rely on shared names must keep them global.If relevant, what needs to be documented once your changes are merged or what have you already documented?
The
examples/cssREADME documents typed CSS modules (JSON +.d.ts) and a native-CSS scoping reference. The Native CSS guide on webpack.js.org may want a note that view-transition names are scoped in CSS modules.Use of AI
Yes. Implemented with AI assistance (Claude Code): cross-bundler investigation of CSS Modules support, the
lib/css/CssParser.jsscoping changes, the tests, and theexamples/csstyped-CSS demonstration. All changes were verified locally — the targeted CSS test suites pass,tscand ESLint are clean, the example builds with the repo tooling, and the generated.d.tswas validated by type-checking a consumer withtsc.Generated by Claude Code