Skip to content

preserve @charset in CSS modules with exportType "text"#20912

Merged
alexander-akait merged 4 commits into
mainfrom
claude/fix-css-modules-charset-BXpYK
May 5, 2026
Merged

preserve @charset in CSS modules with exportType "text"#20912
alexander-akait merged 4 commits into
mainfrom
claude/fix-css-modules-charset-BXpYK

Conversation

@alexander-akait
Copy link
Copy Markdown
Member

The charset extracted by the parser was only re-added for "css-style-sheet"
exports, so "text" exports lost it. Prepend the charset to the exported
text and strip leading @charset from concatenated text imports to avoid
duplicate directives in the resulting string.

The charset extracted by the parser was only re-added for "css-style-sheet"
exports, so "text" exports lost it. Prepend the charset to the exported
text and strip leading @charset from concatenated text imports to avoid
duplicate directives in the resulting string.
Move the @charset handling fully to the build step. The previous commit
added a runtime regex on each concatenated text import to dedupe nested
@charset directives; instead, just bake the @charset into the wrapper at
build time. Nested text-from-text imports may produce multiple @charset
lines in the output, which is still valid CSS (parsers honor only the
first one at byte 0).
Replace the runtime regex with a build-time-computed `.slice(N)` on each
text-from-text import expression. The imported module's @charset value is
known at build time, so the prefix length `'@charset "X";\n'.length` is
emitted as a literal. This produces a single @charset directive at byte 0
of the final output without any regex or extra runtime helper.
Copilot AI review requested due to automatic review settings May 5, 2026 13:48
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 5, 2026

🦋 Changeset detected

Latest commit: 03ea920

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

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

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
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

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

Install it locally:

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

@codecov
Copy link
Copy Markdown

codecov Bot commented May 5, 2026

Codecov Report

❌ Patch coverage is 91.89189% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.35%. Comparing base (756c41a) to head (03ea920).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lib/css/CssGenerator.js 91.89% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #20912      +/-   ##
==========================================
- Coverage   91.37%   91.35%   -0.02%     
==========================================
  Files         563      563              
  Lines       55818    55855      +37     
  Branches    14786    14810      +24     
==========================================
+ Hits        51002    51026      +24     
- Misses       4816     4829      +13     
Flag Coverage Δ
integration 90.30% <91.89%> (-0.02%) ⬇️
test262 45.87% <0.00%> (-0.04%) ⬇️
unit 36.14% <0.00%> (-0.03%) ⬇️

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

☔ View full report in Codecov by Sentry.
📢 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.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates webpack’s CSS generator so exportType: "text" preserves @charset, matching the existing behavior for css-style-sheet exports, and adjusts composed text imports to avoid duplicate leading charset directives.

Changes:

  • Prepend @charset for CSS modules exported as text.
  • Strip a leading @charset from directly imported text exports when composing a text export.
  • Update charset snapshots and add a patch changeset entry.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
lib/css/CssGenerator.js Adds charset preservation for text exports and trims imported text prefixes during concatenation.
test/configCases/css/charset/__snapshots__/ConfigTest.snap Updates expected text-export snapshots to include the preserved charset.
test/configCases/css/charset/__snapshots__/ConfigCacheTest.snap Mirrors the snapshot updates for cached config test cases.
.changeset/css-modules-text-export-charset.md Documents the patch-level behavior change for text exports.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/css/CssGenerator.js Outdated
Comment on lines +173 to +181
const depCharset =
ownCharset !== undefined &&
getDefaultExport &&
depModule.exportType === "text" &&
depModule.buildInfo
? /** @type {BuildInfo} */ (depModule.buildInfo).charset
: undefined;
const charsetPrefixLen =
depCharset !== undefined ? `@charset "${depCharset}";\n`.length : 0;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 03ea920. The slice/prepend now use an effective charset that walks through text imports when a module has no local @charset, and the test fixtures (leaf-with-charset.text.cssinherit-charset.text.cssstyles-7.text.css) cover both the inherited case directly and inherited-then-imported-by-charset-owner case.


Generated by Claude Code

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 5, 2026

Merging this PR will degrade performance by 47.03%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 6 improved benchmarks
❌ 1 regressed benchmark
✅ 137 untouched benchmarks

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "asset-modules-source", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 193.8 KB 365.9 KB -47.03%
Memory benchmark "asset-modules-inline", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 290.9 KB 222.2 KB +30.89%
Memory benchmark "devtool-eval-source-map", scenario '{"name":"mode-development","mode":"development"}' 1.4 MB 1 MB +34.58%
Memory benchmark "side-effects-reexport", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 1,372.6 KB 844.3 KB +62.57%
Memory benchmark "devtool-eval-source-map", scenario '{"name":"mode-production","mode":"production"}' 7.5 MB 6.2 MB +20.32%
Memory benchmark "context-esm", scenario '{"name":"mode-production","mode":"production"}' 8.8 MB 7.1 MB +24.37%
Memory benchmark "three-long", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 1,142 KB 860.4 KB +32.73%

Comparing claude/fix-css-modules-charset-BXpYK (03ea920) with main (b6f54d9)

Open in CodSpeed

A text module can have no local @charset but still expose a default that
starts with @charset because a transitively-imported text module declared
one. Previously the slice computation only used the import's own
buildInfo.charset, missing the inherited case and leaving a duplicate
@charset in the middle of the consumer's text.

Walk through text imports to compute the effective charset and use it for
both the wrapper prepend and the build-time slice length. Add fixtures
covering the transitive case.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Types Coverage

Coverage after merging claude/fix-css-modules-charset-BXpYK into main will be
98.92%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
bin
   webpack.js98.77%100%100%98.77%91
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.js91.30%100%100%91.30%140, 142–143, 145, 147, 153–154, 161, 168, 90
examples/markdown
   webpack.config.mjs100%100%100%100%
examples/typescript
   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-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%37
   ChunkGraph.js100%100%100%100%
   ChunkGroup.js100%100%100%100%
   ChunkTemplate.js100%100%100%100%
   CleanPlugin.js98.72%100%100%98.72%206, 226, 382
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.55%100%100%98.55%1554, 1850, 1857, 1865, 1887, 2783, 3208, 3870, 3899, 3952–3953, 3957, 3962, 3978–3979, 3993–3994, 3999–4000, 4477, 4503, 493, 498, 5211, 5292, 5307, 5332–5333, 5335, 5659, 5664, 5670, 5673, 5685, 5687, 5691, 5707, 5722, 5754, 5808, 5832, 5946, 712–713
   Compiler.js99.55%100%100%99.55%1116–1117, 1125
   ConcatenationScope.js98.59%100%100%98.59%189
   ConditionalInitFragment.js100%100%100%100%
   ConstPlugin.js100%100%100%100%
   ContextExclusionPlugin.js100%100%100%100%
   ContextModule.js100%100%100%100%
   ContextModuleFactory.js97.75%100%100%97.75%258, 393, 418, 443, 447, 458
   ContextReplacementPlugin.js100%100%100%100%
   DefinePlugin.js98.92%100%100%98.92%158–159, 175, 194, 268
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.20%100%100%98.20%379, 425
   DependencyTemplate.js100%100%100%100%
   DependencyTemplates.js100%100%100%100%
   DotenvPlugin.js97.88%100%100%97.88%237, 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.89%100%100%98.89%399–403, 542
   ExternalModuleFactoryPlugin.js100%100%100%100%
   ExternalsPlugin.js100%100%100%100%
   FileSystemInfo.js99.49%100%100%99.49%182, 2213–2214, 2217, 2228, 2239, 2250, 278, 3596, 3611, 3635
   FlagAllModulesAsUsedPlugin.js100%100%100%100%
   FlagDependencyExportsPlugin.js98.74%100%100%98.74%399, 401, 405
   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%
   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.50%100%100%98.50%1304, 1309, 1370, 1384, 1446, 1455
   ModuleFactory.js100%100%100%100%
   ModuleFilenameHelpers.js98.85%100%100%98.85%106, 108
   ModuleGraph.js99.73%100%100%99.73%1004
   ModuleGraphConnection.js100%100%100%100%
   ModuleInfoHeaderPlugin.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.69%100%100%99.69%645
   MultiStats.js100%100%100%100%
   MultiWatching.js100%100%100%100%
   NoEmitOnErrorsPlugin.js100%100%100%100%
   NodeStuffPlugin.js100%100%100%100%
   NormalModule.js97.78%100%100%97.78%1020, 1036, 1123, 1774, 1779–1789, 708, 711, 728, 745, 986
   NormalModuleFactory.js99.47%100%100%99.47%1067, 1376, 466, 478
   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.js98.75%100%100%98.75%446–447, 452, 454, 518
   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.js99.16%100%100%99.16%267–268, 610
   Stats.js100%100%100%100%
   Template.js100%100%100%100%
   TemplatedPathPlugin.js98.86%100%100%98.86%134–135
   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%325
   cli.js98.71%100%100%98.71%117, 469, 501, 543, 813
   index.js100%100%100%100%
   validateSchema.js94.67%100%100%94.67%100, 87, 89, 98
   webpack.js97.22%100%100%97.22%196, 218, 220
lib/asset
   AssetBytesGenerator.js100%100%100%100%
   AssetBytesParser.js100%100%100%100%
   AssetGenerator.js100%100%100%100%
   AssetModulesPlugin.js97.77%100%100%97.77%285, 309, 312, 364, 40
   AssetParser.js100%100%100%100%
   AssetSourceGenerator.js100%100%100%100%
   AssetSourceParser.js100%100%100%100%
   RawDataUrlModule.js100%100%100%100%
lib/async-modules
   AsyncModuleHelpers.js100%100%100%100%
   AwaitDependenciesInitFragment.js100%100%100%100%
   InferAsyncModulesPlugin.js100%100%100%100%
lib/cache
   AddBuildDependenciesPlugin.js100%100%100%100%
   AddManagedPathsPlugin.js100%100%100%100%
   IdleFileCachePlugin.js97.92%100%100%97.92%71, 83, 91
   MemoryCachePlugin.js95.83%100%100%95.83%33
   MemoryWithGcCachePlugin.js93.15%100%100%93.15%106, 113–114, 122, 89
   PackFileCacheStrategy.js96.40%100%100%96.40%1250, 1350, 1354, 1416, 628, 647, 657–659, 661, 677–678, 683, 686, 688, 693, 698, 722, 728, 762, 768, 774, 779, 790, 799, 804–805, 807, 824, 830–831, 833
   ResolverCachePlugin.js100%100%100%100%
   getLazyHashedEtag.js100%100%100%100%
   mergeEtags.js100%100%100%100%
lib/config
   browserslistTargetHandler.js100%100%100%100%
   defaults.js99.13%100%100%99.13%1297–1299, 1307, 270, 273, 278, 282, 468
   normalization.js99%100%100%99%191–192, 258, 273
   target.js100%100%100%100%
lib/container
   ContainerEntryDependency.js100%100%100%100%
   ContainerEntryModule.js100%100%100%100%
   ContainerEntryModuleFactory.js100%100%100%100%
   ContainerExposedDependency.js100%100%100%100%
   ContainerPlugin.js100%100%100%100%
   ContainerReferencePlugin.js100%100%100%100%
   FallbackDependency.js100%100%100%100%
   FallbackItemDependency.js100%100%100%100%
   FallbackModule.js100%100%100%100%
   FallbackModuleFactory.js100%100%100%100%
   HoistContainerReferencesPlugin.js100%100%100%100%
   ModuleFederationPlugin.js100%100%100%100%
   RemoteModule.js100%100%100%100%
   RemoteRuntimeModule.js100%100%100%100%
   

@alexander-akait
Copy link
Copy Markdown
Member Author

@copilot review

Copy link
Copy Markdown

Copilot AI commented May 5, 2026

@copilot review

Here's a summary of the current state of the PR:

What the PR does

  • Prepends @charset "…";\n for exportType: "text" (previously only css-style-sheet got it).
  • Adds _getEffectiveCharset(module, moduleGraph, visited) — a cycle-safe DFS helper that returns the charset that will lead the module's default export. For text modules it first checks buildInfo.charset; if absent it walks CssImportDependency edges so the transitive case (a imports b which has @charset) is handled correctly.
  • When a text module already has a leading @charset, slices the charset prefix off any imported text dep whose own effective charset is also non-null, preventing duplicate directives in the composed string.

Test coverage (all passing — 8 tests, 14 snapshots)

  • leaf-with-charset.text.css — local @charset "UTF-8".
  • inherit-charset.text.css — no local charset; imports leaf-with-charset.text.css (transitive-inherit case).
  • styles-7.text.css — local @charset "UTF-8" + imports inherit-charset.text.css (owns charset, dep inherits it).
  • index.js asserts each composed text starts with exactly one @charset "UTF-8";\n.

No issues found. The implementation correctly handles direct, inherited, and mixed (owner + inheritor) charset scenarios. The cycle guard (WeakSet) prevents infinite loops on circular imports.

@alexander-akait alexander-akait merged commit b34c597 into main May 5, 2026
101 of 105 checks passed
@alexander-akait alexander-akait deleted the claude/fix-css-modules-charset-BXpYK branch May 5, 2026 15:30
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.

3 participants